🛡️ 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.

PatternPurposeExampleBest Practice
try/exceptBasic exception catchingtry: risky_operation() except ValueError: handle_error()Catch specific exceptions
try/except/elseSuccess-only codetry: operation() except: handle() else: success_code()Use for post-operation logic
try/except/finallyGuaranteed cleanuptry: operation() except: handle() finally: cleanup()Always runs cleanup
raiseRe-raise exceptionsexcept Exception as e: log(e); raisePreserve original traceback
assertDebug-time checksassert value > 0, "Value must be positive"Use for development checks
loggingError recordinglogging.error("Operation failed", exc_info=True)Include traceback info
custom exceptionsDomain-specific errorsclass ValidationError(Exception): passClear error categories
context managersResource managementwith 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.

python
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?

😔Poor
🙁Fair
😊Good
😄Great
🤩Excellent