IntermediatePython ยท Lesson 3

Error Handling and Context Managers

Handle exceptions gracefully with try/except/finally, create custom exceptions, and use context managers

Exception Hierarchy

BaseException
โ”œโ”€โ”€ SystemExit
โ”œโ”€โ”€ KeyboardInterrupt
โ””โ”€โ”€ Exception
    โ”œโ”€โ”€ ArithmeticError
    โ”‚   โ”œโ”€โ”€ ZeroDivisionError
    โ”‚   โ””โ”€โ”€ OverflowError
    โ”œโ”€โ”€ LookupError
    โ”‚   โ”œโ”€โ”€ IndexError
    โ”‚   โ””โ”€โ”€ KeyError
    โ”œโ”€โ”€ ValueError
    โ”œโ”€โ”€ TypeError
    โ”œโ”€โ”€ AttributeError
    โ”œโ”€โ”€ NameError
    โ”œโ”€โ”€ OSError
    โ”‚   โ”œโ”€โ”€ FileNotFoundError
    โ”‚   โ””โ”€โ”€ PermissionError
    โ””โ”€โ”€ RuntimeError

try / except / else / finally

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
except (TypeError, ValueError) as e:
    print(f"Type or Value error: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")
else:
    # runs ONLY if no exception was raised
    print(f"Result: {result}")
finally:
    # ALWAYS runs (cleanup)
    print("Done")

Catching and Re-raising

def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        raise ValueError(f"Cannot divide {a} by zero") from None

# Exception chaining:
try:
    open("missing.txt")
except FileNotFoundError as e:
    raise RuntimeError("Config file missing") from e

Custom Exceptions

class ValidationError(ValueError):
    """Raised when validation fails"""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"Validation error on '{field}': {message}")


class AgeError(ValidationError):
    pass


def validate_age(age):
    if not isinstance(age, int):
        raise ValidationError("age", "must be an integer")
    if age < 0 or age > 150:
        raise AgeError("age", f"{age} is out of valid range [0, 150]")
    return age


try:
    validate_age(-5)
except AgeError as e:
    print(f"Field: {e.field}")    # age
    print(f"Error: {e.message}")  # -5 is out of valid range [0, 150]

Context Managers

The with statement ensures proper resource cleanup.

# File handling
with open("data.txt", "w") as f:
    f.write("Hello, World!")
# File automatically closed here, even if an error occurs

# Multiple context managers
with open("input.txt") as infile, open("output.txt", "w") as outfile:
    outfile.write(infile.read())

Creating Context Managers with contextlib

from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.perf_counter()
    try:
        yield  # control passes to the 'with' block here
    finally:
        elapsed = time.perf_counter() - start
        print(f"Elapsed: {elapsed:.4f}s")


with timer():
    time.sleep(0.1)
# Elapsed: 0.1004s

@contextmanager
def temporary_directory():
    import tempfile, shutil
    tmpdir = tempfile.mkdtemp()
    try:
        yield tmpdir
    finally:
        shutil.rmtree(tmpdir)

with temporary_directory() as tmpdir:
    print(f"Working in: {tmpdir}")
# Directory cleaned up automatically

Context Manager Class

class DatabaseConnection:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.connection = None

    def __enter__(self):
        print(f"Connecting to {self.host}:{self.port}")
        self.connection = f"connection_{self.host}"
        return self.connection

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Closing connection")
        self.connection = None
        # Return True to suppress exceptions, False to re-raise
        return False


with DatabaseConnection("localhost", 5432) as conn:
    print(f"Using: {conn}")
# Connecting to localhost:5432
# Using: connection_localhost
# Closing connection

ExceptionGroup โ€” Python 3.11+

# Handle multiple exceptions simultaneously
try:
    raise ExceptionGroup("multiple errors", [
        ValueError("bad value"),
        TypeError("wrong type"),
    ])
except* ValueError as eg:
    print(f"Value errors: {eg.exceptions}")
except* TypeError as eg:
    print(f"Type errors: {eg.exceptions}")

Exercises

Exercise 1: Safe JSON Parser

Write a function that safely parses JSON and returns a default value on failure.

Solution:

import json

def safe_parse(json_str, default=None):
    try:
        return json.loads(json_str)
    except json.JSONDecodeError as e:
        print(f"Parse error: {e}")
        return default

print(safe_parse('{"key": 42}'))     # {'key': 42}
print(safe_parse("invalid json"))    # None

Exercise 2: Retry Decorator

Write a decorator that retries a function up to n times on exception.

Solution:

from functools import wraps
import time

def retry(times=3, delay=0, exceptions=(Exception,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == times - 1:
                        raise
                    if delay:
                        time.sleep(delay)
                    print(f"Attempt {attempt+1} failed: {e}")
        return wrapper
    return decorator

import random

@retry(times=3)
def unreliable():
    if random.random() < 0.8:
        raise ConnectionError("Network error")
    return "Connected!"

Exercise 3: Transaction Context Manager

Implement a simple transaction context manager that rolls back on error.

Solution:

from contextlib import contextmanager

@contextmanager
def transaction(db):
    """Simulates a database transaction"""
    try:
        print("BEGIN TRANSACTION")
        yield db
        print("COMMIT")
    except Exception as e:
        print(f"ROLLBACK (error: {e})")
        raise

class MockDB:
    def execute(self, sql):
        print(f"  SQL: {sql}")

db = MockDB()
try:
    with transaction(db) as conn:
        conn.execute("INSERT INTO users VALUES (1, 'Alice')")
        conn.execute("INSERT INTO users VALUES (2, 'Bob')")
        # raise ValueError("Simulated error")  # uncommenting causes rollback
except ValueError:
    pass