Errors and Exceptions Tutorial: Robust Error Handling in Python
TL;DR
Python handles errors through exceptions using try/except/finally blocks, with built-in exception types like ValueError, TypeError, and FileNotFoundError, plus the ability to create custom exceptions and proper error handling patterns.
Interesting!
Python’s exception handling follows the “Easier to Ask for Forgiveness than Permission” (EAFP) principle - it’s often better to try an operation and handle the exception rather than checking conditions beforehand, leading to cleaner and more efficient code.
Types of Errors in Python
python code snippet start
# Syntax Errors - detected before execution
# print("Hello World" # Missing closing parenthesis
# Runtime Errors - occur during execution
# 1. NameError - undefined variable
# print(undefined_variable)
# 2. TypeError - wrong type operation
# result = "hello" + 5
# 3. ValueError - correct type, wrong value
# number = int("not_a_number")
# 4. IndexError - list index out of range
# my_list = [1, 2, 3]
# print(my_list[10])
# 5. KeyError - dictionary key doesn't exist
# my_dict = {"a": 1, "b": 2}
# print(my_dict["c"])
# 6. FileNotFoundError - file doesn't exist
# with open("nonexistent.txt", "r") as f:
# content = f.read()
# 7. ZeroDivisionError - division by zero
# result = 10 / 0
python code snippet end
Basic Exception Handling
python code snippet start
# Basic try-except block
try:
number = int(input("Enter a number: "))
result = 10 / number
print(f"Result: {result}")
except ValueError:
print("Invalid input! Please enter a valid number.")
except ZeroDivisionError:
print("Cannot divide by zero!")
# Multiple exceptions in one except block
try:
number = int(input("Enter a number: "))
result = 10 / number
print(f"Result: {result}")
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")
# Generic exception handling
try:
# Some risky operation
risky_operation()
except Exception as e:
print(f"An error occurred: {e}")
# Catching specific exception and storing error info
try:
with open("data.txt", "r") as file:
content = file.read()
except FileNotFoundError as e:
print(f"File not found: {e}")
print(f"Error type: {type(e).__name__}")
python code snippet end
The else and finally Clauses
python code snippet start
# else clause - executes if no exception occurs
try:
number = int(input("Enter a number: "))
result = 100 / number
except ValueError:
print("Invalid input!")
except ZeroDivisionError:
print("Cannot divide by zero!")
else:
print(f"Success! Result is {result}")
# This only runs if no exception occurred
# finally clause - always executes
def read_file(filename):
file = None
try:
file = open(filename, 'r')
content = file.read()
return content
except FileNotFoundError:
print(f"File {filename} not found")
return None
except PermissionError:
print(f"Permission denied for {filename}")
return None
finally:
# This always runs, even if exception occurs
if file:
file.close()
print("File closed")
# Complete try-except-else-finally example
def divide_numbers(a, b):
try:
result = a / b
except TypeError:
print("Both arguments must be numbers")
return None
except ZeroDivisionError:
print("Cannot divide by zero")
return None
else:
print("Division successful")
return result
finally:
print("Division operation completed")
print(divide_numbers(10, 2)) # Success case
print(divide_numbers(10, 0)) # Zero division
print(divide_numbers(10, "2")) # Type error
python code snippet end
Raising Exceptions
python code snippet start
# Raising built-in exceptions
def validate_age(age):
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age seems unrealistic")
return True
try:
validate_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
# Re-raising exceptions
def process_data(data):
try:
# Some processing that might fail
result = risky_function(data)
return result
except ValueError as e:
print(f"Logging error: {e}")
raise # Re-raise the same exception
# Raising exceptions with custom messages
def calculate_square_root(number):
if number < 0:
raise ValueError(f"Cannot calculate square root of negative number: {number}")
return number ** 0.5
# Chaining exceptions (Python 3+)
def outer_function():
try:
inner_function()
except ValueError as e:
raise RuntimeError("Outer function failed") from e
def inner_function():
raise ValueError("Inner function error")
try:
outer_function()
except RuntimeError as e:
print(f"Main error: {e}")
print(f"Caused by: {e.__cause__}")
python code snippet end
Custom Exceptions
python code snippet start
# Basic custom exception
class CustomError(Exception):
"""A custom exception class"""
pass
# Custom exception with additional attributes
class ValidationError(Exception):
def __init__(self, message, field_name, value):
super().__init__(message)
self.field_name = field_name
self.value = value
def __str__(self):
return f"Validation failed for {self.field_name}: {self.args[0]} (value: {self.value})"
# Exception hierarchy for a web application
class WebAppError(Exception):
"""Base exception for web application"""
pass
class AuthenticationError(WebAppError):
"""User authentication failed"""
pass
class AuthorizationError(WebAppError):
"""User not authorized for this action"""
pass
class DatabaseError(WebAppError):
"""Database operation failed"""
pass
class APIError(WebAppError):
"""External API call failed"""
def __init__(self, message, status_code=None, response=None):
super().__init__(message)
self.status_code = status_code
self.response = response
# Using custom exceptions
def authenticate_user(username, password):
if not username:
raise AuthenticationError("Username is required")
if not password:
raise AuthenticationError("Password is required")
# Simulate authentication
if username != "admin" or password != "secret":
raise AuthenticationError("Invalid credentials")
return True
def authorize_action(user, action):
if user != "admin":
raise AuthorizationError(f"User {user} not authorized for {action}")
# Exception handling with custom exceptions
try:
authenticate_user("user", "wrong")
except AuthenticationError as e:
print(f"Auth failed: {e}")
except WebAppError as e:
print(f"Web app error: {e}")
python code snippet end
Real-World Exception Handling Patterns
File Operations
python code snippet start
def safe_file_read(filename, encoding='utf-8'):
"""Safely read a file with comprehensive error handling"""
try:
with open(filename, 'r', encoding=encoding) as file:
return file.read()
except FileNotFoundError:
print(f"Error: File '{filename}' not found")
return None
except PermissionError:
print(f"Error: Permission denied for '{filename}'")
return None
except UnicodeDecodeError as e:
print(f"Error: Cannot decode file '{filename}' with {encoding} encoding")
print(f"Try a different encoding. Error: {e}")
return None
except OSError as e:
print(f"Error: OS error reading '{filename}': {e}")
return None
except Exception as e:
print(f"Unexpected error reading '{filename}': {e}")
return None
def safe_file_write(filename, content, encoding='utf-8'):
"""Safely write to a file with error handling"""
try:
with open(filename, 'w', encoding=encoding) as file:
file.write(content)
return True
except PermissionError:
print(f"Error: Permission denied writing to '{filename}'")
return False
except OSError as e:
print(f"Error: Cannot write to '{filename}': {e}")
return False
except Exception as e:
print(f"Unexpected error writing to '{filename}': {e}")
return False
# Usage
content = safe_file_read("data.txt")
if content:
processed_content = content.upper()
if safe_file_write("output.txt", processed_content):
print("File processed successfully")
python code snippet end
Network Operations
python code snippet start
import json
def fetch_api_data(url, timeout=10):
"""Fetch data from API with error handling"""
try:
import requests
response = requests.get(url, timeout=timeout)
response.raise_for_status() # Raises HTTPError for bad status codes
return response.json()
except requests.exceptions.ConnectionError:
raise APIError("Failed to connect to the server")
except requests.exceptions.Timeout:
raise APIError(f"Request timed out after {timeout} seconds")
except requests.exceptions.HTTPError as e:
status_code = e.response.status_code
raise APIError(f"HTTP error {status_code}", status_code, e.response)
except json.JSONDecodeError:
raise APIError("Invalid JSON response from server")
except Exception as e:
raise APIError(f"Unexpected error: {e}")
def handle_api_request(url):
"""Handle API request with proper error handling"""
try:
data = fetch_api_data(url)
return {"success": True, "data": data}
except APIError as e:
error_msg = f"API Error: {e}"
if hasattr(e, 'status_code') and e.status_code:
error_msg += f" (Status: {e.status_code})"
return {"success": False, "error": error_msg}
except Exception as e:
return {"success": False, "error": f"Unexpected error: {e}"}
# Usage
# result = handle_api_request("https://api.example.com/data")
# if result["success"]:
# print(f"Data received: {result['data']}")
# else:
# print(f"Failed to fetch data: {result['error']}")
python code snippet end
Database Operations
python code snippet start
class DatabaseManager:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connection = None
def connect(self):
"""Connect to database with error handling"""
try:
# Simulate database connection
# self.connection = database.connect(self.connection_string)
print("Connected to database")
return True
except Exception as e:
raise DatabaseError(f"Failed to connect to database: {e}")
def execute_query(self, query, parameters=None):
"""Execute database query with error handling"""
if not self.connection:
raise DatabaseError("Not connected to database")
try:
# Simulate query execution
# cursor = self.connection.cursor()
# cursor.execute(query, parameters or [])
# return cursor.fetchall()
print(f"Executing query: {query}")
return [{"id": 1, "name": "test"}] # Mock result
except Exception as e:
# Log the error with context
error_msg = f"Query failed: {query[:100]}..."
if parameters:
error_msg += f" Parameters: {parameters}"
raise DatabaseError(f"{error_msg}. Error: {e}")
def close(self):
"""Close database connection"""
try:
if self.connection:
# self.connection.close()
print("Database connection closed")
except Exception as e:
print(f"Error closing database connection: {e}")
# Usage with context manager
class DatabaseContextManager:
def __init__(self, connection_string):
self.db_manager = DatabaseManager(connection_string)
def __enter__(self):
self.db_manager.connect()
return self.db_manager
def __exit__(self, exc_type, exc_val, exc_tb):
self.db_manager.close()
# Handle any exceptions that occurred in the with block
if exc_type is not None:
print(f"Exception in database context: {exc_val}")
return False # Don't suppress exceptions
# Usage
try:
with DatabaseContextManager("connection_string") as db:
results = db.execute_query("SELECT * FROM users WHERE active = ?", [True])
print(f"Found {len(results)} users")
except DatabaseError as e:
print(f"Database operation failed: {e}")
python code snippet end
Debugging and Logging Errors
python code snippet start
import logging
import traceback
from datetime import datetime
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('app.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def log_exception(func):
"""Decorator to log exceptions"""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"Exception in {func.__name__}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise
return wrapper
@log_exception
def risky_function(data):
"""Function that might raise exceptions"""
if not data:
raise ValueError("Data cannot be empty")
# Simulate processing
return data.upper()
# Exception context for debugging
def detailed_error_handler():
"""Demonstrate detailed error information"""
try:
risky_function("")
except Exception as e:
# Get detailed exception information
exc_type, exc_value, exc_traceback = sys.exc_info()
print(f"Exception Type: {exc_type.__name__}")
print(f"Exception Value: {exc_value}")
print(f"Exception Args: {exc_value.args}")
# Print traceback
print("\nTraceback:")
traceback.print_exc()
# Get formatted traceback as string
tb_str = traceback.format_exc()
logger.error(f"Detailed error info:\n{tb_str}")
# Error aggregation for monitoring
class ErrorTracker:
def __init__(self):
self.errors = []
def log_error(self, error, context=None):
"""Log error with context for monitoring"""
error_info = {
'timestamp': datetime.now().isoformat(),
'type': type(error).__name__,
'message': str(error),
'context': context or {},
'traceback': traceback.format_exc()
}
self.errors.append(error_info)
logger.error(f"Error logged: {error_info}")
def get_error_summary(self):
"""Get summary of errors for monitoring"""
if not self.errors:
return "No errors recorded"
error_counts = {}
for error in self.errors:
error_type = error['type']
error_counts[error_type] = error_counts.get(error_type, 0) + 1
return {
'total_errors': len(self.errors),
'error_types': error_counts,
'latest_error': self.errors[-1] if self.errors else None
}
# Global error tracker
error_tracker = ErrorTracker()
def process_user_data(user_data):
"""Process user data with error tracking"""
try:
# Validate user data
if 'email' not in user_data:
raise ValidationError("Email is required", "email", None)
if '@' not in user_data['email']:
raise ValidationError("Invalid email format", "email", user_data['email'])
# Process the data
return {"status": "success", "user_id": 123}
except ValidationError as e:
error_tracker.log_error(e, {"user_data": user_data})
return {"status": "error", "message": str(e)}
except Exception as e:
error_tracker.log_error(e, {"user_data": user_data})
return {"status": "error", "message": "Internal server error"}
# Usage
# result = process_user_data({"name": "John"}) # Missing email
# print(error_tracker.get_error_summary())
python code snippet end
Best Practices for Exception Handling
python code snippet start
# 1. Be specific with exceptions
# Bad - too generic
try:
value = int(user_input)
except: # Don't catch all exceptions
print("Error occurred")
# Good - specific exceptions
try:
value = int(user_input)
except ValueError:
print("Invalid number format")
except TypeError:
print("Input must be a string")
# 2. Don't ignore exceptions silently
# Bad
try:
risky_operation()
except Exception:
pass # Silent failure is dangerous
# Good
try:
risky_operation()
except Exception as e:
logger.error(f"Operation failed: {e}")
# Handle appropriately
# 3. Use specific exception types
def validate_positive_number(value):
if not isinstance(value, (int, float)):
raise TypeError(f"Expected number, got {type(value).__name__}")
if value <= 0:
raise ValueError(f"Expected positive number, got {value}")
return True
# 4. Provide helpful error messages
class ConfigurationError(Exception):
def __init__(self, setting_name, value, reason):
message = f"Invalid configuration for '{setting_name}': {reason} (value: {value})"
super().__init__(message)
self.setting_name = setting_name
self.value = value
self.reason = reason
# 5. Use context managers for resource management
def read_config_file(filename):
"""Properly handle file resources"""
try:
with open(filename, 'r') as file: # Automatic cleanup
return json.load(file)
except FileNotFoundError:
raise ConfigurationError("config_file", filename, "file not found")
except json.JSONDecodeError as e:
raise ConfigurationError("config_file", filename, f"invalid JSON: {e}")
# 6. Chain exceptions for context
def load_user_settings(user_id):
try:
config = read_config_file(f"user_{user_id}.json")
return config
except ConfigurationError as e:
raise RuntimeError(f"Failed to load settings for user {user_id}") from e
# 7. Validate inputs early
def calculate_interest(principal, rate, time):
# Validate inputs first
if not all(isinstance(x, (int, float)) for x in [principal, rate, time]):
raise TypeError("All parameters must be numbers")
if principal <= 0:
raise ValueError("Principal must be positive")
if rate < 0:
raise ValueError("Rate cannot be negative")
if time <= 0:
raise ValueError("Time must be positive")
return principal * (1 + rate) ** time
python code snippet end
Testing Exception Handling
python code snippet start
import unittest
class TestExceptionHandling(unittest.TestCase):
def test_value_error_raised(self):
"""Test that ValueError is raised for invalid input"""
with self.assertRaises(ValueError):
calculate_square_root(-1)
def test_exception_message(self):
"""Test that exception has correct message"""
with self.assertRaises(ValueError) as context:
validate_age(-5)
self.assertIn("negative", str(context.exception))
def test_custom_exception_attributes(self):
"""Test custom exception attributes"""
with self.assertRaises(ValidationError) as context:
raise ValidationError("Test error", "test_field", "test_value")
error = context.exception
self.assertEqual(error.field_name, "test_field")
self.assertEqual(error.value, "test_value")
def test_exception_handling_behavior(self):
"""Test that function handles exceptions correctly"""
result = process_user_data({}) # Invalid data
self.assertEqual(result["status"], "error")
self.assertIn("Email is required", result["message"])
# Run tests
if __name__ == "__main__":
unittest.main()
python code snippet end
Proper exception handling makes your Python programs robust, maintainable, and user-friendly by gracefully managing errors and providing meaningful feedback.
Combine robust exception handling with Python's logging module to create production-ready applications that can diagnose and recover from problems effectively. These patterns integrate with control flow structures and support Zen of Python principle that “errors should never pass silently.”
Reference: Python Tutorial - Errors and Exceptions