Error Handling

Try-Except Blocks

Handle errors gracefully to prevent your program from crashing.

# Basic try-except
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero!")

# Multiple exception types
try:
    number = int(input("Enter a number: "))
    result = 10 / number
    print(f"Result: {result}")
except ValueError:
    print("Please enter a valid number")
except ZeroDivisionError:
    print("Cannot divide by zero")

# Catching multiple exceptions
try:
    # Some risky operation
    pass
except (ValueError, TypeError, ZeroDivisionError) as e:
    print(f"An error occurred: {e}")

Exception Hierarchy and Custom Exceptions

# Custom exception classes
class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Insufficient funds: ${balance} < ${amount}")

class InvalidAgeError(Exception):
    pass

# Using custom exceptions
class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
    
    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        return f"Withdrew ${amount}. Balance: ${self.balance}"

# Handling custom exceptions
account = BankAccount(100)
try:
    print(account.withdraw(150))
except InsufficientFundsError as e:
    print(f"Transaction failed: {e}")
    print(f"Available balance: ${e.balance}")

Finally and Else Clauses

# Complete try-except structure
def process_file(filename):
    file = None
    try:
        file = open(filename, 'r')
        content = file.read()
        # Process content
        return content.upper()
    except FileNotFoundError:
        print(f"File {filename} not found")
        return None
    except PermissionError:
        print(f"Permission denied for {filename}")
        return None
    else:
        # Runs only if no exception occurred
        print("File processed successfully")
    finally:
        # Always runs, regardless of exceptions
        if file:
            file.close()
            print("File closed")

# Better approach using context managers
def process_file_better(filename):
    try:
        with open(filename, 'r') as file:
            content = file.read()
            return content.upper()
    except FileNotFoundError:
        print(f"File {filename} not found")
        return None
    except PermissionError:
        print(f"Permission denied for {filename}")
        return None
    else:
        print("File processed successfully")

Raising Exceptions

# 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

# Re-raising exceptions
def divide_numbers(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Logging: Division by zero attempted")
        raise  # Re-raise the same exception

# Using assertions
def calculate_square_root(number):
    assert number >= 0, "Number must be non-negative"
    return number ** 0.5

try:
    validate_age(-5)
except ValueError as e:
    print(f"Validation error: {e}")

try:
    result = divide_numbers(10, 0)
except ZeroDivisionError:
    print("Division by zero handled")

Exception Chaining

# Exception chaining with 'from'
def parse_config(config_string):
    try:
        import json
        return json.loads(config_string)
    except json.JSONDecodeError as e:
        raise ValueError("Invalid configuration format") from e

def load_user_settings(config_string):
    try:
        config = parse_config(config_string)
        return config['user_settings']
    except ValueError as e:
        raise RuntimeError("Failed to load user settings") from e

# Using exception chaining
try:
    settings = load_user_settings("invalid json")
except RuntimeError as e:
    print(f"Error: {e}")
    print(f"Caused by: {e.__cause__}")
    print(f"Original error: {e.__cause__.__cause__}")

Logging Errors

import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def safe_divide(a, b):
    try:
        result = a / b
        logging.info(f"Division successful: {a} / {b} = {result}")
        return result
    except ZeroDivisionError:
        logging.error(f"Division by zero: {a} / {b}")
        return None
    except Exception as e:
        logging.exception(f"Unexpected error in division: {a} / {b}")
        return None

# Error handling with context
class DatabaseConnection:
    def __enter__(self):
        logging.info("Connecting to database")
        # Simulate connection
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            logging.error(f"Database error: {exc_val}")
        logging.info("Closing database connection")
        return False  # Don't suppress exceptions
    
    def query(self, sql):
        if "DROP" in sql.upper():
            raise ValueError("DROP statements not allowed")
        return f"Results for: {sql}"

# Using the context manager
try:
    with DatabaseConnection() as db:
        result = db.query("SELECT * FROM users")
        print(result)
except ValueError as e:
    logging.error(f"Query error: {e}")
💡 Best Practices:
← Classes Next: Decorators →