python

examples

examples.py🐍
"""
08 - Error Handling: Examples
Run this file to see exception handling in action!
"""

print("=" * 60)
print("ERROR HANDLING - EXAMPLES")
print("=" * 60)

# =============================================================================
# 1. BASIC TRY-EXCEPT
# =============================================================================
print("\n--- 1. Basic Try-Except ---\n")

# Without error handling - would crash!
# result = 10 / 0  # ZeroDivisionError!

# With error handling
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Caught ZeroDivisionError: Cannot divide by zero!")

# Catching any exception
try:
    numbers = [1, 2, 3]
    print(numbers[10])
except Exception as e:
    print(f"Caught exception: {type(e).__name__}: {e}")

# =============================================================================
# 2. MULTIPLE EXCEPTION TYPES
# =============================================================================
print("\n--- 2. Multiple Exception Types ---\n")

def risky_operation(value):
    """A function that might raise different exceptions."""
    try:
        # Could raise TypeError, ZeroDivisionError, or ValueError
        if value == "text":
            return int("not a number")
        elif value == 0:
            return 10 / value
        elif value < 0:
            raise ValueError("Negative values not allowed")
        else:
            return 10 / value
    except ZeroDivisionError:
        print("  Error: Division by zero!")
        return None
    except ValueError as e:
        print(f"  Error: Invalid value - {e}")
        return None
    except TypeError:
        print("  Error: Wrong type!")
        return None

print(f"risky_operation(2) = {risky_operation(2)}")
print(f"risky_operation(0) = {risky_operation(0)}")
print(f"risky_operation(-5) = {risky_operation(-5)}")
print(f"risky_operation('text') = {risky_operation('text')}")

# Catching multiple exceptions in one except block
try:
    # Some operation
    x = int("not a number")
except (ValueError, TypeError) as e:
    print(f"\nCaught ValueError or TypeError: {e}")

# =============================================================================
# 3. ELSE AND FINALLY CLAUSES
# =============================================================================
print("\n--- 3. Else and Finally Clauses ---\n")

def divide(a, b):
    """Demonstrate try-except-else-finally."""
    try:
        result = a / b
    except ZeroDivisionError:
        print(f"  Cannot divide {a} by {b}")
        return None
    else:
        # Only runs if NO exception was raised
        print(f"  Division successful!")
        return result
    finally:
        # ALWAYS runs, exception or not
        print(f"  Attempted to divide {a} by {b}")

print("Testing divide(10, 2):")
print(f"  Result: {divide(10, 2)}")

print("\nTesting divide(10, 0):")
print(f"  Result: {divide(10, 0)}")

# =============================================================================
# 4. RAISING EXCEPTIONS
# =============================================================================
print("\n--- 4. Raising Exceptions ---\n")

def validate_age(age):
    """Validate that age is a reasonable value."""
    if not isinstance(age, int):
        raise TypeError(f"Age must be an integer, got {type(age).__name__}")
    if age < 0:
        raise ValueError("Age cannot be negative")
    if age > 150:
        raise ValueError("Age is unrealistically high")
    return True

# Test validation
test_ages = [25, -5, 200, "thirty"]
for age in test_ages:
    try:
        validate_age(age)
        print(f"  Age {age}: Valid")
    except (TypeError, ValueError) as e:
        print(f"  Age {age}: Invalid - {e}")

# =============================================================================
# 5. RE-RAISING EXCEPTIONS
# =============================================================================
print("\n--- 5. Re-raising Exceptions ---\n")

def process_data(data):
    """Process data, log errors, and re-raise."""
    try:
        # Simulate processing
        result = int(data)
        return result * 2
    except ValueError:
        print(f"  [LOG] Failed to process: {data}")
        raise  # Re-raise the same exception

try:
    process_data("not_a_number")
except ValueError as e:
    print(f"  Caller caught re-raised exception: {e}")

# =============================================================================
# 6. CUSTOM EXCEPTIONS
# =============================================================================
print("\n--- 6. Custom Exceptions ---\n")

# Define custom exception classes
class ValidationError(Exception):
    """Exception for validation errors."""
    pass

class InsufficientFundsError(Exception):
    """Exception when account has insufficient funds."""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(
            f"Cannot withdraw ${amount:.2f}: only ${balance:.2f} available"
        )

class BankAccount:
    """Simple bank account with custom exceptions."""
    
    def __init__(self, balance=0):
        if balance < 0:
            raise ValidationError("Initial balance cannot be negative")
        self.balance = balance
    
    def withdraw(self, amount):
        if amount <= 0:
            raise ValidationError("Withdrawal amount must be positive")
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        return self.balance

# Test custom exceptions
try:
    account = BankAccount(100)
    print(f"  Created account with balance: ${account.balance:.2f}")
    
    account.withdraw(30)
    print(f"  After withdrawing $30: ${account.balance:.2f}")
    
    account.withdraw(100)  # This will fail
except InsufficientFundsError as e:
    print(f"  Error: {e}")
    print(f"  Details: Balance=${e.balance:.2f}, Attempted=${e.amount:.2f}")

# =============================================================================
# 7. EXCEPTION CHAINING
# =============================================================================
print("\n--- 7. Exception Chaining ---\n")

def fetch_user(user_id):
    """Simulate fetching user that might fail."""
    if user_id < 0:
        raise ValueError(f"Invalid user ID: {user_id}")
    return {"id": user_id, "name": "Alice"}

def process_user_request(user_id):
    """Process request, wrapping low-level errors."""
    try:
        user = fetch_user(user_id)
        return f"Processed request for {user['name']}"
    except ValueError as e:
        # Chain with 'from' to preserve original exception
        raise RuntimeError("Failed to process user request") from e

try:
    process_user_request(-1)
except RuntimeError as e:
    print(f"  Caught: {e}")
    print(f"  Caused by: {e.__cause__}")

# =============================================================================
# 8. EXCEPTION HIERARCHY
# =============================================================================
print("\n--- 8. Exception Hierarchy ---\n")

# All exceptions inherit from BaseException
# Most inherit from Exception
# More specific exceptions inherit from general ones

print("Exception hierarchy (partial):")
print("  BaseException")
print("    ├── SystemExit")
print("    ├── KeyboardInterrupt")
print("    └── Exception")
print("        ├── ArithmeticError")
print("        │   ├── ZeroDivisionError")
print("        │   └── OverflowError")
print("        ├── LookupError")
print("        │   ├── IndexError")
print("        │   └── KeyError")
print("        ├── ValueError")
print("        ├── TypeError")
print("        └── OSError")
print("            ├── FileNotFoundError")
print("            └── PermissionError")

# Catch parent to catch children
try:
    numbers = [1, 2, 3]
    print(numbers[10])
except LookupError:  # Catches IndexError (and KeyError)
    print("\n  Caught LookupError (covers IndexError and KeyError)")

# =============================================================================
# 9. CONTEXT MANAGERS FOR CLEANUP
# =============================================================================
print("\n--- 9. Context Managers for Cleanup ---\n")

# 'with' statement ensures cleanup even on exceptions
class ManagedResource:
    """A resource that needs cleanup."""
    
    def __init__(self, name):
        self.name = name
        print(f"  Acquiring resource: {name}")
    
    def __enter__(self):
        print(f"  Entering context for: {self.name}")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"  Releasing resource: {self.name}")
        if exc_type:
            print(f"  Exception occurred: {exc_type.__name__}: {exc_val}")
        # Return True to suppress exception, False to propagate
        return False
    
    def do_work(self, fail=False):
        if fail:
            raise RuntimeError("Work failed!")
        print(f"  Working with: {self.name}")

# Successful operation
print("Successful operation:")
with ManagedResource("Database") as resource:
    resource.do_work()

# Operation with exception
print("\nOperation with exception:")
try:
    with ManagedResource("Database") as resource:
        resource.do_work(fail=True)
except RuntimeError:
    print("  Exception was propagated to caller")

# =============================================================================
# 10. ASSERTION ERRORS
# =============================================================================
print("\n--- 10. Assertions ---\n")

def calculate_average(numbers):
    """Calculate average with assertion for validation."""
    assert len(numbers) > 0, "Cannot calculate average of empty list"
    assert all(isinstance(n, (int, float)) for n in numbers), "All items must be numbers"
    return sum(numbers) / len(numbers)

# Valid case
result = calculate_average([1, 2, 3, 4, 5])
print(f"  Average of [1,2,3,4,5]: {result}")

# Invalid case
try:
    calculate_average([])
except AssertionError as e:
    print(f"  Assertion failed: {e}")

# Note: Assertions can be disabled with python -O
print("\n  Note: Use assertions for debugging, not for input validation")

# =============================================================================
# 11. EXCEPTION IN LOOPS
# =============================================================================
print("\n--- 11. Exceptions in Loops ---\n")

data = ["1", "2", "three", "4", "five", "6"]

# Continue on error
print("Processing with continue:")
results = []
for item in data:
    try:
        results.append(int(item))
    except ValueError:
        print(f"  Skipping invalid item: {item}")
        continue

print(f"  Valid results: {results}")

# Collect all errors
print("\nCollecting all errors:")
results = []
errors = []
for i, item in enumerate(data):
    try:
        results.append(int(item))
    except ValueError as e:
        errors.append(f"Index {i}: {item} - {e}")

print(f"  Results: {results}")
print(f"  Errors: {errors}")

# =============================================================================
# 12. PRACTICAL PATTERNS
# =============================================================================
print("\n--- 12. Practical Patterns ---\n")

# Pattern 1: Default on error
def safe_int(value, default=0):
    """Convert to int with default on error."""
    try:
        return int(value)
    except (ValueError, TypeError):
        return default

print(f"safe_int('42') = {safe_int('42')}")
print(f"safe_int('abc') = {safe_int('abc')}")
print(f"safe_int('abc', -1) = {safe_int('abc', -1)}")

# Pattern 2: Retry with backoff
import time

def retry_operation(func, max_retries=3, delay=0.1):
    """Retry a function on failure."""
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            print(f"  Attempt {attempt + 1} failed: {e}")
            if attempt < max_retries - 1:
                time.sleep(delay)
    raise Exception(f"All {max_retries} attempts failed")

# Simulate flaky operation
attempt_count = 0
def flaky_operation():
    global attempt_count
    attempt_count += 1
    if attempt_count < 3:
        raise ConnectionError("Network error")
    return "Success!"

print("\nRetry pattern:")
result = retry_operation(flaky_operation)
print(f"  Final result: {result}")

# Pattern 3: Exception as control flow (EAFP - Easier to Ask Forgiveness)
print("\nEAFP vs LBYL:")

# LBYL (Look Before You Leap) - Check first
def get_value_lbyl(data, key):
    if key in data:
        return data[key]
    return None

# EAFP (Easier to Ask Forgiveness than Permission) - Try first
def get_value_eafp(data, key):
    try:
        return data[key]
    except KeyError:
        return None

data = {"a": 1, "b": 2}
print(f"  LBYL: {get_value_lbyl(data, 'c')}")
print(f"  EAFP: {get_value_eafp(data, 'c')}")
print("  Python prefers EAFP style")

# =============================================================================
# 13. LOGGING EXCEPTIONS
# =============================================================================
print("\n--- 13. Logging Exceptions ---\n")

import logging

# Configure basic logging
logging.basicConfig(
    level=logging.DEBUG,
    format='  %(levelname)s: %(message)s'
)

logger = logging.getLogger(__name__)

def risky_function():
    """Function that might fail."""
    try:
        result = 1 / 0
    except ZeroDivisionError:
        # Log with exception info
        logger.exception("An error occurred in risky_function")
        return None

result = risky_function()

# =============================================================================
# 14. WARNINGS
# =============================================================================
print("\n--- 14. Warnings ---\n")

import warnings

def deprecated_function():
    """A function that will be removed in future versions."""
    warnings.warn(
        "deprecated_function is deprecated, use new_function instead",
        DeprecationWarning,
        stacklevel=2
    )
    return "Old result"

# Capture warnings
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    result = deprecated_function()
    if w:
        print(f"  Warning: {w[0].message}")
        print(f"  Category: {w[0].category.__name__}")

print("\n" + "=" * 60)
print("END OF EXAMPLES")
print("=" * 60)
Examples - Python Tutorial | DeepML