Error handling is one of the most important aspects of writing reliable Python code. Whether you are building web applications, automation scripts, or data pipelines, understanding how to properly handle exceptions will make your code more robust and maintainable.
Understanding Python Exceptions
In Python, exceptions are events that occur during program execution that disrupt the normal flow. When an error occurs, Python creates an exception object containing information about what went wrong.
# This will raise a ZeroDivisionError
result = 10 / 0
# This will raise a FileNotFoundError
with open("nonexistent.txt") as f:
data = f.read()
Basic Try-Except Pattern
The fundamental pattern for handling exceptions uses the try-except block:
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
result = 0
Catching Multiple Exceptions
You can handle different exception types with separate handlers:
try:
value = int(user_input)
result = 100 / value
except ValueError:
print("Please enter a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")
except Exception as e:
print(f"Unexpected error: {e}")
The Try-Except-Else-Finally Pattern
Python provides a complete error handling structure with else and finally clauses:
try:
file = open("config.json", "r")
data = json.load(file)
except FileNotFoundError:
print("Config file not found, using defaults")
data = default_config
except json.JSONDecodeError:
print("Invalid JSON in config file")
data = default_config
else:
print("Config loaded successfully")
finally:
if 'file' in locals():
file.close()
Creating Custom Exceptions
For application-specific errors, create custom exception classes:
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class DatabaseConnectionError(Exception):
def __init__(self, host, port):
self.host = host
self.port = port
super().__init__(f"Cannot connect to {host}:{port}")
# Usage
def validate_email(email):
if "@" not in email:
raise ValidationError("email", "Invalid email format")
Context Managers for Resource Handling
Use context managers to ensure proper resource cleanup:
from contextlib import contextmanager
@contextmanager
def database_connection(host, port):
conn = None
try:
conn = connect(host, port)
yield conn
except ConnectionError as e:
logging.error(f"Database connection failed: {e}")
raise
finally:
if conn:
conn.close()
# Usage
with database_connection("localhost", 5432) as db:
db.execute("SELECT * FROM users")
Logging Exceptions Properly
Always log exceptions with full traceback information:
import logging
logger = logging.getLogger(__name__)
try:
process_data(raw_data)
except Exception as e:
logger.exception("Failed to process data")
# This logs the full traceback automatically
Best Practices
- Be specific: Catch specific exceptions rather than bare
exceptclauses - Do not silence errors: Always log or handle exceptions meaningfully
- Use finally for cleanup: Ensure resources are released regardless of exceptions
- Create custom exceptions: Define application-specific exception hierarchies
- Raise exceptions early: Validate inputs at the boundary of your functions
- Use context managers: Leverage
withstatements for automatic resource management - Chain exceptions: Use
raise ... from ...to preserve exception context
Proper error handling separates professional code from hobby projects. By applying these patterns consistently, you will write Python applications that handle failures gracefully and provide clear diagnostic information when things go wrong.