Practical Techniques for Effective Error Handling
Hierarchical Exception Handling
When dealing with multiple exception types, you can organize them in a hierarchy to handle them more effectively. This allows you to catch and handle exceptions at different levels of granularity.
class CustomException(Exception):
pass
class SpecificException(CustomException):
pass
try:
## Code that might raise a SpecificException
pass
except SpecificException:
## Handle the SpecificException
pass
except CustomException:
## Handle the broader CustomException
pass
Chaining Exceptions
In some cases, you may want to provide additional context or information when an exception is raised. You can chain exceptions using the raise from
syntax to maintain the original exception while adding more details.
def process_input(input_data):
try:
## Process the input data
pass
except ValueError as e:
raise CustomException("Error processing input data.") from e
try:
process_input("invalid_data")
except CustomException as e:
print(e)
print(e.__cause__) ## Prints the original ValueError exception
Handling Exceptions at Different Levels
When designing your application's architecture, consider handling exceptions at the appropriate level. This can involve catching and handling exceptions in individual functions, modules, or at the top-level of your application.
## Function-level exception handling
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError("Error: Division by zero.") from e
## Module-level exception handling
def process_data(data):
try:
result = divide(data, 0)
except ValueError as e:
logging.error(e)
return None
## Top-level exception handling
if __name__ == "__main__":
try:
result = process_data(10)
except Exception as e:
print("An unexpected error occurred:", e)
Using Context Managers
Context managers, created with the with
statement, can simplify exception handling by automatically handling the cleanup or resource acquisition/release process. This can be particularly useful when working with resources like files, network connections, or database transactions.
from contextlib import contextmanager
@contextmanager
def open_file(filename):
try:
file = open(filename, 'r')
yield file
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
finally:
file.close()
with open_file('nonexistent_file.txt') as f:
content = f.read()
Handling Exceptions in Asynchronous Code
When working with asynchronous code in Python (e.g., using async
/await
), you need to consider how to handle exceptions effectively. This may involve using try-except
blocks within your coroutines or using exception handling techniques at the event loop level.
import asyncio
async def process_data(data):
try:
## Asynchronous processing of data
result = await some_async_operation(data)
except ValueError as e:
print(f"Error processing data: {e}")
return None
return result
async def main():
try:
result = await process_data(10)
except Exception as e:
print(f"Unexpected error occurred: {e}")
asyncio.run(main())
Documenting Exception Handling
Clearly document your exception handling strategy, including the types of exceptions you expect, how they are handled, and any custom exception classes you've defined. This information can be valuable for other developers working on the project.
def divide(a, b):
"""
Divide two numbers.
Args:
a (int): The dividend.
b (int): The divisor.
Returns:
float: The result of the division.
Raises:
ZeroDivisionError: If the divisor is zero.
ValueError: If the divisor is not a number.
"""
if not isinstance(b, (int, float)):
raise ValueError("Divisor must be a number.")
if b == 0:
raise ZeroDivisionError("Division by zero.")
return a / b