Python Exception Handling
Learn how to handle errors and exceptions in Python. Master try-except blocks, custom exceptions, and robust error management techniques.
What are Exceptions?
Exceptions are errors that occur during program execution. Python provides mechanisms to handle these exceptions gracefully, preventing program crashes and enabling recovery.
"Exception handling allows programs to continue running despite errors."
Basic Try-Except
Use try-except to handle exceptions:
# Basic exception handling
try:
result = 10 / 0
print(result)
except ZeroDivisionError:
print("Cannot divide by zero!")
# Multiple exception types
try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type conversion error")
# Catch all exceptions (not recommended)
try:
risky_operation()
except Exception as e:
print(f"An error occurred: {e}")
# Using else clause
try:
result = 10 / 2
except ZeroDivisionError:
print("Division by zero")
else:
print(f"Result: {result}") # Executes if no exception
# Finally clause (always executes)
try:
file = open("example.txt", "r")
content = file.read()
except FileNotFoundError:
print("File not found")
finally:
if 'file' in locals():
file.close()
print("Cleanup completed")
Common Built-in Exceptions
Python's built-in exception hierarchy:
| Exception | Description | Example |
|---|---|---|
| ZeroDivisionError | Division by zero | 10 / 0 |
| ValueError | Invalid value for operation | int("abc") |
| TypeError | Operation on wrong type | "a" + 1 |
| IndexError | List index out of range | list[10] |
| KeyError | Dictionary key not found | dict["missing"] |
| FileNotFoundError | File doesn't exist | open("missing.txt") |
| AttributeError | Invalid attribute access | obj.missing_method() |
| ImportError | Module import failure | import nonexistent |
| NameError | Undefined variable | print(undefined_var) |
Raising Exceptions
Use raise to throw exceptions:
# Raise built-in exception
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
# Raise with custom message
def validate_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age seems unrealistic")
# Re-raising exceptions
try:
risky_function()
except Exception as e:
print(f"Error occurred: {e}")
raise # Re-raises the same exception
# Raising different exception
try:
result = api_call()
except ConnectionError:
raise RuntimeError("Service unavailable") from None
Custom Exceptions
Create your own exception classes:
# Basic custom exception
class CustomError(Exception):
pass
# Custom exception with message
class ValidationError(Exception):
def __init__(self, message, field=None):
super().__init__(message)
self.field = field
# Using custom exceptions
def validate_user(user_data):
if not user_data.get('name'):
raise ValidationError("Name is required", "name")
if not user_data.get('email'):
raise ValidationError("Email is required", "email")
if '@' not in user_data['email']:
raise ValidationError("Invalid email format", "email")
try:
validate_user({"name": "John"})
except ValidationError as e:
print(f"Validation error in {e.field}: {e}")
# Exception hierarchy
class ApplicationError(Exception):
pass
class DatabaseError(ApplicationError):
pass
class NetworkError(ApplicationError):
pass
# Catching hierarchy
try:
database_operation()
except DatabaseError:
print("Database issue")
except NetworkError:
print("Network issue")
except ApplicationError:
print("General application error")
Context Managers and Exceptions
Use context managers for resource management:
# File handling with context manager
with open('file.txt', 'r') as file:
content = file.read()
# File automatically closed even if exception occurs
# Custom context manager
class DatabaseConnection:
def __enter__(self):
self.connection = connect_to_database()
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
self.connection.close()
if exc_type:
print(f"Exception occurred: {exc_val}")
return False # Propagate exception
return True
with DatabaseConnection() as conn:
# Use connection
pass
# Using contextlib
from contextlib import contextmanager
@contextmanager
def database_connection():
conn = connect_to_database()
try:
yield conn
finally:
conn.close()
with database_connection() as conn:
# Use connection
pass
Exception Chaining
Link related exceptions:
# Exception chaining with 'from'
try:
process_data()
except ValueError as e:
raise RuntimeError("Data processing failed") from e
# Suppress chaining with 'from None'
try:
api_call()
except Exception:
raise ValueError("Invalid API response") from None
# Accessing chained exceptions
try:
risky_operation()
except Exception as e:
print(f"Original exception: {e}")
if e.__cause__:
print(f"Caused by: {e.__cause__}")
if e.__context__:
print(f"During handling of: {e.__context__}")
Best Practices
- Be specific: Catch specific exceptions, not Exception
- Use finally: For cleanup operations
- Don't suppress exceptions: Let them propagate when appropriate
- Provide meaningful messages: Help with debugging
- Use context managers: For resource management
- Log exceptions: For debugging and monitoring
- Fail fast: Don't continue with invalid state
- Test exception handling: Ensure it works correctly
Advanced Exception Patterns
# Retry pattern
import time
def retry_operation(max_attempts=3, delay=1):
for attempt in range(max_attempts):
try:
return risky_operation()
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(delay)
# Circuit breaker pattern
class CircuitBreaker:
def __init__(self, failure_threshold=5):
self.failure_threshold = failure_threshold
self.failure_count = 0
self.state = 'closed'
def call(self, func, *args, **kwargs):
if self.state == 'open':
raise RuntimeError("Circuit breaker is open")
try:
result = func(*args, **kwargs)
self.failure_count = 0
return result
except Exception as e:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self.state = 'open'
raise
# Exception grouping
from contextlib import ExitStack
def process_files(file_list):
with ExitStack() as stack:
files = []
try:
for filename in file_list:
file = stack.enter_context(open(filename, 'r'))
files.append(file)
# Process all files
return process_all_files(files)
except Exception:
# All files automatically closed
raise
# Logging exceptions
import logging
logging.basicConfig(level=logging.ERROR)
def log_and_reraise(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logging.error(f"Error in {func.__name__}: {e}", exc_info=True)
raise
return wrapper
@log_and_reraise
def critical_operation():
# Some operation that might fail
pass
Exception handling is crucial for building robust Python applications. Proper exception handling prevents crashes, provides meaningful error messages, and enables graceful recovery from errors.