
Mastering Python Exception Handling and Logging for Production Applications
Advanced Exception Handling Patterns
Python's exception handling system provides powerful mechanisms for managing errors, but proper usage requires understanding several advanced patterns. The key is to distinguish between expected failures and unexpected errors, and to handle each appropriately.
Custom Exception Classes
Creating domain-specific exceptions improves code clarity and enables targeted error handling:
class ValidationError(Exception):
"""Raised when data validation fails."""
def __init__(self, field, value, message):
self.field = field
self.value = value
self.message = message
super().__init__(f"Validation error in {field}: {message}")
class DatabaseConnectionError(Exception):
"""Raised when database connection fails."""
def __init__(self, host, port, reason):
self.host = host
self.port = port
self.reason = reason
super().__init__(f"Failed to connect to {host}:{port} - {reason}")
# Usage example
def validate_email(email):
if '@' not in email:
raise ValidationError('email', email, 'Missing @ symbol')
return TrueException Chaining and Context Preservation
When wrapping exceptions, preserve the original context using raise ... from ... syntax:
def process_user_data(user_id):
try:
user_data = fetch_user_from_db(user_id)
return process_user_data(user_data)
except DatabaseError as e:
# Preserve original exception while adding context
raise DataProcessingError(f"Failed to process user {user_id}") from e
except ProcessingError as e:
# Re-raise with additional context
raise DataProcessingError(f"User {user_id} processing failed") from eStructured Logging Best Practices
Logging in production applications requires structured approaches that balance verbosity with performance. The key is to log meaningful information without overwhelming system resources.
Context-Aware Logging
Use structured logging to include contextual information with each log entry:
import logging
import json
from datetime import datetime
# Configure structured logging
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
def log_user_action(user_id, action, details=None):
"""Log user actions with structured context."""
log_data = {
'timestamp': datetime.utcnow().isoformat(),
'user_id': user_id,
'action': action,
'details': details or {}
}
logger.info('User action performed', extra={'data': log_data})
# Usage
log_user_action(12345, 'login', {'ip_address': '192.168.1.100'})Performance-Optimized Logging
Avoid expensive operations in log statements by using lazy evaluation:
import logging
logger = logging.getLogger(__name__)
def expensive_operation():
# Simulate expensive computation
return [i**2 for i in range(10000)]
# Bad practice - always evaluates expensive_operation()
logger.debug(f"Processing data: {expensive_operation()}")
# Good practice - only evaluates when debug level is enabled
logger.debug("Processing data: %s", expensive_operation())Error Recovery and Retry Patterns
Production applications often need to handle transient failures gracefully. Implementing smart retry mechanisms with exponential backoff prevents cascading failures.
Retry Decorator Implementation
import time
import random
from functools import wraps
from typing import Type, Tuple
def retry(max_attempts: int = 3,
backoff_factor: float = 1.0,
exceptions: Tuple[Type[Exception], ...] = (Exception,)):
"""
Decorator for retrying function calls with exponential backoff.
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt == max_attempts - 1:
break
# Exponential backoff with jitter
delay = backoff_factor * (2 ** attempt)
jitter = random.uniform(0, 0.1 * delay)
time.sleep(delay + jitter)
logger.warning(
f"Attempt {attempt + 1} failed, retrying in {delay:.2f}s",
extra={'exception': str(e)}
)
raise last_exception
return wrapper
return decorator
# Usage example
@retry(max_attempts=3, backoff_factor=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch_api_data(url):
# Simulate API call that might fail
import requests
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()Comprehensive Error Management Strategy
A robust error management strategy combines multiple techniques for maximum effectiveness:
| Strategy | Purpose | Implementation |
|---|---|---|
| Custom Exceptions | Domain-specific error handling | Inherit from Exception |
| Exception Chaining | Preserve error context | Use raise ... from ... |
| Structured Logging | Audit trail and debugging | Include context data |
| Retry Mechanisms | Handle transient failures | Exponential backoff |
| Graceful Degradation | Maintain functionality | Fallback behavior |
| Monitoring Integration | Alert on critical issues | Log to monitoring systems |
Complete Error Handling Example
import logging
import time
from typing import Optional, Dict, Any
from contextlib import contextmanager
class ApplicationError(Exception):
"""Base application exception."""
pass
class ServiceUnavailableError(ApplicationError):
"""Raised when external service is unavailable."""
pass
logger = logging.getLogger(__name__)
@contextmanager
def error_context(operation_name: str):
"""Context manager for tracking operation errors."""
start_time = time.time()
try:
yield
except Exception as e:
duration = time.time() - start_time
logger.error(
f"Operation {operation_name} failed after {duration:.2f}s",
extra={
'operation': operation_name,
'duration': duration,
'error_type': type(e).__name__,
'error_message': str(e)
}
)
raise
def process_order(order_id: int, payment_data: Dict[str, Any]) -> bool:
"""Process an order with comprehensive error handling."""
with error_context(f"process_order_{order_id}"):
# Validate input
if not payment_data.get('card_number'):
raise ValidationError('card_number', payment_data, 'Missing card number')
# Process payment (may fail transiently)
payment_result = None
try:
payment_result = process_payment(payment_data)
except PaymentError as e:
logger.warning("Payment processing failed, retrying", extra={'order_id': order_id})
# Retry logic here
raise ServiceUnavailableError("Payment service temporarily unavailable") from e
# Update order status
update_order_status(order_id, 'processed', payment_result)
return True
def process_payment(payment_data: Dict[str, Any]) -> Dict[str, Any]:
"""Simulate payment processing."""
# Simulate occasional failures
import random
if random.random() < 0.1: # 10% failure rate
raise PaymentError("Payment gateway timeout")
return {'status': 'success', 'transaction_id': 'txn_' + str(time.time())}Testing Exception Handling
Proper exception handling requires comprehensive testing to ensure reliability:
import unittest
from unittest.mock import patch, MagicMock
class TestErrorHandling(unittest.TestCase):
def test_validation_error_raised(self):
"""Test that validation errors are properly raised."""
with self.assertRaises(ValidationError) as context:
validate_email('invalid-email')
self.assertEqual(context.exception.field, 'email')
self.assertIn('Missing @ symbol', str(context.exception))
@patch('your_module.fetch_user_from_db')
def test_database_connection_error(self, mock_fetch):
"""Test handling of database connection failures."""
mock_fetch.side_effect = DatabaseConnectionError('localhost', 5432, 'Connection refused')
with self.assertRaises(DataProcessingError):
process_user_data(12345)
@patch('requests.get')
def test_retry_mechanism(self, mock_get):
"""Test retry mechanism with transient failures."""
# First two calls fail, third succeeds
mock_get.side_effect = [
ConnectionError("Network error"),
TimeoutError("Timeout"),
MagicMock(status_code=200, json=lambda: {'data': 'success'})
]
# This should succeed after retries
result = fetch_api_data('http://example.com/api')
self.assertEqual(result['data'], 'success')Learn more with useful resources
- Python Logging HOWTO - Official Python documentation for comprehensive logging practices
- Effective Python: 90 Specific Ways to Write Better Python - Excellent resource for Python best practices including error handling
- Python Exception Handling Patterns - Real Python's detailed guide to exception handling in production applications
