Exception handling is a key concept in Python used to manage errors and exceptions that occur during the execution of a program. Proper exception handling allows your program to respond gracefully to unexpected situations without crashing.
Why Exception Handling?
Errors in Python are categorized as exceptions. If an exception is not handled, it will terminate the program and produce an error message, which includes a traceback of the error. Using exception handling, you can manage these errors and prevent abrupt program crashes.
For example, dividing a number by zero raises a ZeroDivisionError. Without exception handling, the program would crash:
print(1 / 0) # ZeroDivisionError: division by zero
With exception handling, we can handle this scenario more gracefully:
try:
print(1 / 0)
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
The try-except Block
The basic syntax for handling exceptions in Python involves the try-except block:
try:
# Code that may raise an exception
result = 10 / 0
except ZeroDivisionError:
# Code that handles the exception
print("Cannot divide by zero.")
In this example:
- The code inside the try block is executed.
- If a ZeroDivisionError occurs, the code in the except block is executed instead of raising the exception.
Multiple Exceptions
You can handle multiple exceptions by specifying different except blocks for each type of exception:
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero.")
except ValueError:
print("Invalid input.")
Alternatively, you can catch multiple exceptions in one except block using a tuple:
try:
result = 10 / 0
except (ZeroDivisionError, ValueError):
print("An error occurred.")
Catching All Exceptions
You can catch any exception by using a generic except block without specifying a type. However, this practice is generally discouraged as it can hide unexpected errors:
try:
result = 10 / 0
except Exception as e:
print(f"An error occurred: {e}")
In this case, e captures the exception instance, and you can print or log it for debugging.
The else Clause
The else clause can be used to specify code that should run if no exception is raised in the try block:
try:
result = 10 / 2
except ZeroDivisionError:
print("Cannot divide by zero.")
else:
print(f"Result: {result}") # Only executed if no exception occurs
The finally Clause
The finally block is always executed, whether an exception occurs or not. It’s typically used to clean up resources, like closing files or network connections:
try:
file = open("data.txt", "r")
data = file.read()
except FileNotFoundError:
print("File not found.")
finally:
file.close() # This will always run, even if an exception occurs
The finally block ensures that cleanup code runs, regardless of whether an exception was raised.
Raising Exceptions
You can manually raise exceptions using the raise keyword. This is useful when you want to trigger an exception conditionally:
def check_positive(number: int) -> None:
if number < 0:
raise ValueError("Number must be positive!")
print(f"Number is: {number}")
check_positive(-5) # Raises a ValueError
You can also raise exceptions while preserving the original traceback, which helps in debugging:
try:
raise ValueError("Initial error")
except ValueError as e:
raise RuntimeError("Secondary error occurred") from e
Custom Exceptions
Python allows you to define custom exceptions by subclassing the built-in Exception class. This is useful when you want to handle domain-specific errors:
class InvalidAgeError(Exception):
"""Custom exception for invalid age."""
pass
def set_age(age: int) -> None:
if age < 0:
raise InvalidAgeError("Age cannot be negative!")
print(f"Age is: {age}")
try:
set_age(-1)
except InvalidAgeError as e:
print(e) # Outputs: Age cannot be negative!
Creating custom exceptions provides more control over error handling in complex systems.
Common Mistakes
- Catching all exceptions with except: This can lead to obscure bugs because it catches exceptions that you might not want to handle (e.g., KeyboardInterrupt, SystemExit).
# Problem
try:
result = 1 / 0
except:
print("Error occurred.") # Catches all exceptions, including system ones.
# Solution
try:
result = 1 / 0
except ZeroDivisionError:
print("Cannot divide by zero.")
- Raising exceptions without a message: Always provide informative messages when raising exceptions. It makes debugging easier.
# Problem
raise ValueError # No message provided
# Solution
raise ValueError("A detailed error message") # With message
- Using finally for non-cleanup tasks: The finally block should only be used for cleanup tasks like closing files or freeing up resources. Avoid using it for logic that could affect the main flow of the program.