🛡️ Error Handling
Error handling in Python allows your programs to gracefully manage unexpected situations and continue running instead of crashing. Using try/except blocks, you can catch errors, provide helpful feedback to users, and maintain application stability even when things go wrong.
Think of error handling as your program's safety net - it catches problems before they crash your application and provides meaningful responses that help users understand what happened and how to fix it.
# Basic try-except structure
def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
return "Error: Cannot divide by zero"
except TypeError:
return "Error: Please provide numbers only"
# Multiple exception handling
def safe_file_operation(filename):
try:
with open(filename, 'r') as file:
content = file.read()
return len(content)
except FileNotFoundError:
print(f"File '{filename}' not found")
except PermissionError:
print(f"Permission denied accessing '{filename}'")
except Exception as e:
print(f"Unexpected error: {e}")
return None
# Testing error handling
print(safe_divide(10, 2)) # Returns 5.0
print(safe_divide(10, 0)) # Returns error message
print(safe_divide("10", 2)) # Returns error message
result = safe_file_operation("nonexistent.txt")
🎯 Understanding Python Exceptions
Python's exception hierarchy provides systematic error categorization that enables precise error handling and debugging strategies.
Exception Type Categories
Different exception categories serve specific purposes in error identification and require tailored handling approaches.
def handle_runtime_errors():
"""Demonstrate common runtime error handling"""
test_cases = [
("list_access", lambda: [1, 2, 3][5]),
("dict_access", lambda: {"name": "Alice"}["age"]),
("zero_division", lambda: 10 / 0),
("type_error", lambda: "hello" + 5),
("attribute_error", lambda: (42).split())
]
for test_name, operation in test_cases:
try:
result = operation()
print(f"{test_name}: Success - {result}")
except IndexError:
print(f"{test_name}: Index out of range")
except KeyError:
print(f"{test_name}: Key not found in dictionary")
except ZeroDivisionError:
print(f"{test_name}: Cannot divide by zero")
except TypeError:
print(f"{test_name}: Incompatible data types")
except AttributeError:
print(f"{test_name}: Object has no such attribute")
except Exception as e:
print(f"{test_name}: Unexpected error - {e}")
handle_runtime_errors()
Input Validation with Error Handling
Input validation prevents invalid data from causing errors and provides immediate feedback for correction.
def validate_user_input(data_type, prompt):
"""Robust input validation with error handling"""
# Simulate user input for demonstration
test_inputs = {
"int": ["25", "-5", "abc", "100"],
"email": ["user@example.com", "invalid-email", "test@test.com"],
"age": ["25", "150", "-5", "abc"]
}
inputs = test_inputs.get(data_type, ["25"])
for user_input in inputs:
try:
print(f"\nTesting input: '{user_input}' for {data_type}")
if data_type == "int":
value = int(user_input)
if value < 0:
raise ValueError("Number must be non-negative")
print(f"✅ Valid integer: {value}")
elif data_type == "email":
if "@" not in user_input or "." not in user_input:
raise ValueError("Invalid email format")
print(f"✅ Valid email: {user_input}")
elif data_type == "age":
age = int(user_input)
if not 0 <= age <= 150:
raise ValueError("Age must be between 0 and 150")
print(f"✅ Valid age: {age}")
except ValueError as e:
print(f"❌ Invalid input: {e}")
except Exception as e:
print(f"❌ Unexpected error: {e}")
# Test validation
validate_user_input("int", "Enter a number: ")
validate_user_input("email", "Enter email: ")
validate_user_input("age", "Enter age: ")
⚡ Advanced Error Handling Techniques
Exception Information and Debugging
Exception objects contain valuable debugging information that helps identify error causes and locations.
import traceback
import sys
def demonstrate_exception_info():
"""Show how to extract detailed exception information"""
try:
data = {"users": [{"name": "Alice"}]}
# This will cause a KeyError
email = data["users"][0]["email"]
except KeyError as e:
print(f"Exception Details:")
print(f" Type: {type(e).__name__}")
print(f" Message: {e}")
print(f" Args: {e.args}")
print(f"\nTraceback Information:")
exc_type, exc_value, exc_traceback = sys.exc_info()
print(f" File: {exc_traceback.tb_frame.f_code.co_filename}")
print(f" Line: {exc_traceback.tb_lineno}")
print(f" Function: {exc_traceback.tb_frame.f_code.co_name}")
print(f"\nFull Traceback:")
traceback.print_exc()
demonstrate_exception_info()
Custom Exception Classes
Creating custom exceptions provides specific error types for your application's unique error conditions.
class ValidationError(Exception):
"""Custom exception for validation failures"""
def __init__(self, message, field_name=None, value=None):
super().__init__(message)
self.field_name = field_name
self.value = value
self.message = message
def __str__(self):
if self.field_name:
return f"Validation error in '{self.field_name}': {self.message}"
return self.message
class BusinessLogicError(Exception):
"""Custom exception for business rule violations"""
def __init__(self, message, error_code=None):
super().__init__(message)
self.error_code = error_code
self.message = message
def validate_user_registration(user_data):
"""Validate user registration with custom exceptions"""
# Email validation
email = user_data.get('email', '')
if not email:
raise ValidationError("Email is required", "email", email)
if '@' not in email:
raise ValidationError("Invalid email format", "email", email)
# Age validation
age = user_data.get('age')
if age is None:
raise ValidationError("Age is required", "age", age)
if not isinstance(age, int):
raise ValidationError("Age must be a number", "age", age)
if age < 13:
raise BusinessLogicError("Users must be at least 13 years old", "AGE_RESTRICTION")
return True
# Test custom exceptions
test_users = [
{"email": "alice@example.com", "age": 25}, # Valid
{"email": "", "age": 25}, # Missing email
{"email": "alice@example.com", "age": 10}, # Too young
{"email": "invalid-email", "age": 25}, # Invalid email
]
for i, user in enumerate(test_users):
try:
validate_user_registration(user)
print(f"User {i+1}: Registration successful")
except ValidationError as e:
print(f"User {i+1}: Validation failed - {e}")
except BusinessLogicError as e:
print(f"User {i+1}: Business rule violation - {e} (Code: {e.error_code})")
🚀 Error Handling Patterns
Graceful Degradation and Recovery
Implement error handling that maintains functionality even when some features fail.
import json
import logging
class ConfigurationManager:
"""Configuration manager with graceful error handling"""
def __init__(self, config_file="app_config.json"):
self.config_file = config_file
self.config = {}
self.load_config()
def load_config(self):
"""Load configuration with fallback to defaults"""
try:
with open(self.config_file, 'r') as file:
self.config = json.load(file)
print(f"✅ Configuration loaded from {self.config_file}")
except FileNotFoundError:
print(f"⚠️ Config file not found. Using defaults.")
self.config = self.get_default_config()
except json.JSONDecodeError as e:
print(f"⚠️ Invalid JSON in config file: {e}")
print("Using default configuration.")
self.config = self.get_default_config()
except PermissionError:
print(f"⚠️ Permission denied reading config file.")
self.config = self.get_default_config()
except Exception as e:
print(f"⚠️ Unexpected error loading config: {e}")
self.config = self.get_default_config()
def get_default_config(self):
"""Return default configuration"""
return {
"database": {"host": "localhost", "port": 5432},
"cache": {"enabled": True, "timeout": 300},
"logging": {"level": "INFO", "file": "app.log"}
}
def get(self, key, default=None):
"""Get configuration value with error handling"""
try:
keys = key.split('.')
value = self.config
for k in keys:
value = value[k]
return value
except (KeyError, TypeError):
print(f"⚠️ Configuration key '{key}' not found. Using default: {default}")
return default
def save_config(self):
"""Save configuration with error handling"""
try:
with open(self.config_file, 'w') as file:
json.dump(self.config, file, indent=2)
print(f"✅ Configuration saved to {self.config_file}")
return True
except PermissionError:
print(f"❌ Permission denied writing to {self.config_file}")
return False
except Exception as e:
print(f"❌ Error saving configuration: {e}")
return False
# Test graceful degradation
config_manager = ConfigurationManager("nonexistent_config.json")
# Get configuration values
db_host = config_manager.get("database.host", "localhost")
cache_enabled = config_manager.get("cache.enabled", False)
invalid_setting = config_manager.get("invalid.key", "fallback_value")
print(f"\nConfiguration values:")
print(f"Database host: {db_host}")
print(f"Cache enabled: {cache_enabled}")
print(f"Invalid setting: {invalid_setting}")
Context Managers for Resource Cleanup
Use context managers to ensure proper resource cleanup even when errors occur.
class DatabaseConnection:
"""Simulate database connection with error handling"""
def __init__(self, host, port):
self.host = host
self.port = port
self.connected = False
def __enter__(self):
try:
print(f"Connecting to {self.host}:{self.port}")
# Simulate connection
self.connected = True
return self
except Exception as e:
print(f"Failed to connect: {e}")
raise
def __exit__(self, exc_type, exc_value, traceback):
if self.connected:
print(f"Closing connection to {self.host}:{self.port}")
self.connected = False
if exc_type:
print(f"Exception occurred: {exc_type.__name__}: {exc_value}")
return False # Don't suppress exceptions
def execute_query(self, query):
if not self.connected:
raise ConnectionError("Not connected to database")
# Simulate query execution
if "invalid" in query.lower():
raise ValueError("Invalid SQL query")
return f"Query executed: {query}"
# Test context manager with error handling
queries = [
"SELECT * FROM users",
"INVALID QUERY",
"UPDATE users SET active = true"
]
for query in queries:
try:
with DatabaseConnection("localhost", 5432) as db:
result = db.execute_query(query)
print(f"✅ {result}")
except ConnectionError as e:
print(f"❌ Connection error: {e}")
except ValueError as e:
print(f"❌ Query error: {e}")
except Exception as e:
print(f"❌ Unexpected error: {e}")
print()
🌟 Production Error Handling
Logging and Monitoring
Proper logging helps track errors in production environments for debugging and monitoring.
import logging
import datetime
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(), # Console output
# logging.FileHandler('app.log') # File output (uncommented in real apps)
]
)
logger = logging.getLogger(__name__)
class ProductionErrorHandler:
"""Production-ready error handling with logging"""
def __init__(self):
self.error_count = 0
def handle_user_request(self, user_id, action):
"""Handle user request with comprehensive error logging"""
try:
logger.info(f"Processing request: user_id={user_id}, action={action}")
# Simulate different types of errors
if action == "invalid_action":
raise ValueError("Unsupported action requested")
elif action == "permission_denied":
raise PermissionError("User lacks required permissions")
elif action == "server_error":
raise RuntimeError("Internal server error")
elif user_id is None:
raise ValueError("User ID is required")
# Simulate successful processing
result = f"Action '{action}' completed for user {user_id}"
logger.info(f"Request successful: {result}")
return {"status": "success", "result": result}
except ValueError as e:
self.error_count += 1
logger.warning(f"Validation error for user {user_id}: {e}")
return {
"status": "error",
"error_type": "validation_error",
"message": str(e),
"user_id": user_id
}
except PermissionError as e:
self.error_count += 1
logger.error(f"Permission denied for user {user_id}: {e}")
return {
"status": "error",
"error_type": "permission_error",
"message": "Access denied",
"user_id": user_id
}
except Exception as e:
self.error_count += 1
logger.critical(f"Unexpected error for user {user_id}: {e}")
return {
"status": "error",
"error_type": "internal_error",
"message": "An unexpected error occurred",
"user_id": user_id
}
def get_error_stats(self):
"""Return error statistics"""
return {"total_errors": self.error_count}
# Test production error handling
handler = ProductionErrorHandler()
test_requests = [
(123, "login"),
(456, "invalid_action"),
(789, "permission_denied"),
(None, "login"),
(999, "server_error")
]
print("Production Error Handling Test:")
print("=" * 40)
for user_id, action in test_requests:
response = handler.handle_user_request(user_id, action)
print(f"User {user_id}, Action '{action}': {response['status']}")
if response['status'] == 'error':
print(f" Error: {response['message']}")
print(f"\nError Statistics: {handler.get_error_stats()}")
Essential Error Handling Methods and Patterns
Understanding core error handling techniques enables robust application development with proper exception management.
Pattern | Purpose | Example | Best Practice |
---|---|---|---|
try/except | Basic exception catching | try: risky_operation() except ValueError: handle_error() | Catch specific exceptions |
try/except/else | Success-only code | try: operation() except: handle() else: success_code() | Use for post-operation logic |
try/except/finally | Guaranteed cleanup | try: operation() except: handle() finally: cleanup() | Always runs cleanup |
raise | Re-raise exceptions | except Exception as e: log(e); raise | Preserve original traceback |
assert | Debug-time checks | assert value > 0, "Value must be positive" | Use for development checks |
logging | Error recording | logging.error("Operation failed", exc_info=True) | Include traceback info |
custom exceptions | Domain-specific errors | class ValidationError(Exception): pass | Clear error categories |
context managers | Resource management | with resource: operation() | Automatic cleanup |
Hands-on Exercise
Create a simple age validator that handles different types of input errors. Build a function that asks for age input and handles invalid entries like non-numbers, negative values, and unrealistic ages.
def get_valid_age():
# TODO: Get age input from user
# TODO: Handle non-numeric input (ValueError)
# TODO: Check for negative ages
# TODO: Check for unrealistic ages (over 150)
# TODO: Return valid age
pass
def validate_age_input(age_string):
# TODO: Convert string to integer
# TODO: Raise ValueError for invalid conversions
# TODO: Check age ranges
# TODO: Return validated age
pass
# TODO: Test your functions
print("Age Validation Test:")
test_inputs = ["25", "abc", "-5", "200", "30"]
for test_input in test_inputs:
try:
age = validate_age_input(test_input)
print(f"Input '{test_input}': Valid age {age}")
except ValueError as e:
print(f"Input '{test_input}': Error - {e}")
except Exception as e:
print(f"Input '{test_input}': Unexpected error - {e}")
Solution and Explanation 💡
Click to see the complete solution
def get_valid_age():
# Get age input from user with error handling
while True:
try:
age_input = input("Please enter your age: ")
age = validate_age_input(age_input)
return age
except ValueError as e:
print(f"Error: {e}")
print("Please try again.")
except KeyboardInterrupt:
print("\nInput cancelled.")
return None
def validate_age_input(age_string):
# Convert string to integer
try:
age = int(age_string)
except ValueError:
raise ValueError("Age must be a valid number")
# Check for negative ages
if age < 0:
raise ValueError("Age cannot be negative")
# Check for unrealistic ages (over 150)
if age > 150:
raise ValueError("Age cannot be over 150 years")
# Return validated age
return age
# Test your functions
print("Age Validation Test:")
test_inputs = ["25", "abc", "-5", "200", "30"]
for test_input in test_inputs:
try:
age = validate_age_input(test_input)
print(f"Input '{test_input}': Valid age {age}")
except ValueError as e:
print(f"Input '{test_input}': Error - {e}")
except Exception as e:
print(f"Input '{test_input}': Unexpected error - {e}")
# Interactive test (uncomment to test with real input)
# print("\nInteractive test:")
# valid_age = get_valid_age()
# if valid_age is not None:
# print(f"Your age is: {valid_age}")
Key Learning Points:
- 📌 try/except blocks: Catch specific exceptions like ValueError
- 📌 Input validation: Check data ranges and types before processing
- 📌 Error messages: Provide clear, helpful error messages to users
- 📌 Exception hierarchy: Handle specific exceptions before general ones
- 📌 User experience: Loop until valid input is provided
Learn more about getting user input to handle user interactions robustly with proper validation and error handling.
Test Your Knowledge
Test what you've learned about error handling:
What's Next?
Now that you can handle errors effectively, you're ready to explore getting user input. Learn how to create interactive programs that validate user input and provide engaging user experiences.
Ready to continue? Check out our lesson on Getting User Input.
Was this helpful?
Track Your Learning Progress
Sign in to bookmark tutorials and keep track of your learning journey.
Your progress is saved automatically as you read.