Python Error Handling: Best Practices and Patterns
Good error handling makes your code robust and maintainable. Here's how to handle errors properly in Python:
1. Basic Try-Except
# Catch specific exceptions ✅
try:
result = int(user_input)
except ValueError:
print("Please enter a valid number")
# Multiple exceptions
try:
data = json.loads(file_content)
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"Error loading data: {e}")
2. Don't Catch All Exceptions
# Bad: Hides bugs ❌
try:
process_data()
except:
pass
# Bad: Too broad ❌
try:
process_data()
except Exception:
pass
# Good: Specific exceptions ✅
try:
process_data()
except (ValueError, KeyError) as e:
logger.error(f"Data processing failed: {e}")
raise
3. Try-Except-Else-Finally
try:
file = open('data.txt', 'r')
except FileNotFoundError:
print("File not found")
else:
# Runs only if no exception
content = file.read()
print(f"Read {len(content)} characters")
finally:
# Always runs (cleanup)
if 'file' in locals():
file.close()
# Better: Use context manager
try:
with open('data.txt', 'r') as file:
content = file.read()
except FileNotFoundError:
print("File not found")
4. Common Exception Types
# ValueError: Invalid value
try:
age = int("not a number")
except ValueError as e:
print(f"Invalid input: {e}")
# KeyError: Missing dictionary key
try:
value = my_dict['missing_key']
except KeyError:
value = my_dict.get('missing_key', 'default')
# TypeError: Wrong type
try:
result = "text" + 5
except TypeError:
result = "text" + str(5)
# FileNotFoundError: File doesn't exist
try:
with open('missing.txt') as f:
data = f.read()
except FileNotFoundError:
data = "default content"
# IndexError: List index out of range
try:
item = my_list[10]
except IndexError:
item = None
5. Raising Exceptions
# Raise built-in exceptions
def process_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
if not isinstance(age, int):
raise TypeError("Age must be an integer")
return age
# Re-raise with context
try:
data = fetch_data()
except ConnectionError as e:
logger.error("Failed to fetch data")
raise # Re-raises original exception
# Raise from another exception
try:
parse_config()
except json.JSONDecodeError as e:
raise ConfigError("Invalid config file") from e
6. Custom Exceptions
# Define custom exceptions
class DataValidationError(Exception):
"""Raised when data validation fails"""
pass
class DatabaseConnectionError(Exception):
"""Raised when database connection fails"""
def __init__(self, host, message="Cannot connect"):
self.host = host
self.message = f"{message} to {host}"
super().__init__(self.message)
# Use custom exceptions
def validate_email(email):
if '@' not in email:
raise DataValidationError(f"Invalid email: {email}")
return email
7. Practical Patterns
# Safe dictionary access
def get_user_name(user_id):
try:
return users[user_id]['name']
except (KeyError, TypeError):
return "Unknown User"
# Retry logic
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
try:
return requests.get(url, timeout=5)
except requests.RequestException as e:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt) # Exponential backoff
# Validate and convert
def safe_int(value, default=0):
try:
return int(value)
except (ValueError, TypeError):
return default
# Usage
age = safe_int(user_input, default=18)
8. Context Managers for Cleanup
# Custom context manager
from contextlib import contextmanager
@contextmanager
def database_connection(db_name):
conn = connect_to_db(db_name)
try:
yield conn
finally:
conn.close()
# Usage
with database_connection('users') as conn:
data = conn.query('SELECT * FROM users')
# Connection auto-closed even if error occurs
Best Practices
- ✅ Catch specific exceptions, not
Exception - ✅ Use context managers (
with) for resources - ✅ Log errors before re-raising
- ✅ Provide meaningful error messages
- ✅ Create custom exceptions for domain logic
- ❌ Don't use exceptions for control flow
- ❌ Don't silently ignore errors with
pass - ❌ Don't catch exceptions you can't handle
Pro Tip: It's easier to ask for forgiveness than permission (EAFP). In Python, use try-except rather than checking if something exists first. It's more Pythonic and often faster.
← Back to Python Tips