You open a file, read it, then must close it. Forgetting to close leaks resources. Context managers with with automatically close resources when done - even if exceptions occur. No more manual try-finally for cleanup.

Basic with statement

Let Python handle resource cleanup.

basic_with.py
# Basic with statement usage

def main():
    # Simulated file handling
    class FakeFile:
        def __init__(self, name, mode):
            self.name = name
            self.mode = mode
            self.closed = False
        
        def __enter__(self):
            print(f"  Opening {self.name}")
            return self
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            print(f"  Closing {self.name}")
            self.closed = True
            return False  # Don't suppress exceptions
        
        def read(self):
            if self.closed:
                raise ValueError("File is closed")
            return f"Contents of {self.name}"
    
    print("Basic with statement:\n")
    
    # Using with statement
    with FakeFile("data.txt", "r") as f:
        content = f.read()
        print(f"  Read: {content}")
    
    print(f"  File closed: {f.closed}")
    
    
    # Without with statement (manual)
    print("\nManual resource management:")
    
    f2 = FakeFile("manual.txt", "r")
    f2.__enter__()
    try:
        content = f2.read()
        print(f"  Read: {content}")
    finally:
        f2.__exit__(None, None, None)
    
    # Multiple context managers
    print("\nMultiple resources:")
    
    with FakeFile("file1.txt", "r") as f1, FakeFile("file2.txt", "r") as f2:
        print(f"  Reading {f1.name}")
        print(f"  Reading {f2.name}")
    
    print("  Both files closed")
    
    # Exception handling
    print("\nWith exception:")
    
    try:
        with FakeFile("error.txt", "r") as f:
            print(f"  Inside with block")
            raise ValueError("Simulated error")
    except ValueError as e:
        print(f"  Caught: {e}")
    
    print(f"  File was still closed: {f.closed}")
    
    # Timer context
    import time
    
    class Timer:
        def __enter__(self):
            self.start = time.time()
            print("  Timer started")
            return self
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            self.elapsed = time.time() - self.start
            print(f"  Timer stopped: {self.elapsed:.4f}s")
            return False
    
    print("\nTiming code:")
    
    with Timer():
        # Simulate work
        total = sum(range(1000))
        print(f"  Computed sum: {total}")

if __name__ == "__main__":
    main()

with open(file) as f: automatically closes file when block exits.

with statement Automatic resource management: `with resource as r:`. Cleanup guaranteed.

Class-based context manager

Create your own with enter and exit.

class_based.py
# Class-based context managers

def main():
    # Simple resource manager
    class Resource:
        def __init__(self, name):
            self.name = name
            self.acquired = False
        
        def __enter__(self):
            print(f"  Acquiring {self.name}")
            self.acquired = True
            return self
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            print(f"  Releasing {self.name}")
            self.acquired = False
            return False  # Propagate exceptions
        
        def use(self):
            if not self.acquired:
                raise RuntimeError("Resource not acquired")
            print(f"  Using {self.name}")
    
    print("Resource management:\n")
    
    with Resource("Database") as db:
        db.use()
        print(f"  Acquired: {db.acquired}")
    
    print(f"  After with: {db.acquired}")
    
    
    # Context manager with return value
    class Connection:
        def __init__(self, host, port):
            self.host = host
            self.port = port
            self.connected = False
        
        def __enter__(self):
            print(f"  Connecting to {self.host}:{self.port}")
            self.connected = True
            return self  # Returns self for use in 'as' clause
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            print(f"  Disconnecting from {self.host}:{self.port}")
            self.connected = False
            return False
        
        def send(self, data):
            if not self.connected:
                raise RuntimeError("Not connected")
            print(f"  Sent: {data}")
    
    print("\nConnection:")
    
    with Connection("localhost", 8080) as conn:
        conn.send("Hello")
        conn.send("World")
    
    # Exception information in __exit__
    class ErrorHandler:
        def __enter__(self):
            print("  Entering context")
            return self
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            if exc_type is None:
                print("  Exiting normally")
            else:
                print(f"  Exception occurred: {exc_type.__name__}")
                print(f"  Message: {exc_val}")
            return False  # Don't suppress
    
    print("\nError handling:")
    
    with ErrorHandler():
        print("  No error")
    
    print()
    
    try:
        with ErrorHandler():
            print("  About to raise error")
            raise ValueError("Test error")
    except ValueError:
        print("  Exception propagated")
    
    # Suppress exception by returning True
    class Suppressor:
        def __enter__(self):
            return self
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            if exc_type is ValueError:
                print(f"  Suppressing {exc_type.__name__}: {exc_val}")
                return True  # Suppress ValueError
            return False  # Propagate other exceptions
    
    print("\nSuppressing exceptions:")
    
    with Suppressor():
        print("  Raising ValueError")
        raise ValueError("This will be suppressed")
    
    print("  Execution continued")
    
    # Lock-like context manager
    class Lock:
        def __init__(self, name):
            self.name = name
            self.locked = False
        
        def __enter__(self):
            print(f"  Acquiring lock: {self.name}")
            self.locked = True
            return self
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            print(f"  Releasing lock: {self.name}")
            self.locked = False
            return False
    
    print("\nLocking:")
    
    lock = Lock("data_lock")
    
    with lock:
        print(f"  Lock held: {lock.locked}")
        # Critical section
    
    print(f"  Lock released: {lock.locked}")

if __name__ == "__main__":
    main()

__enter__ sets up, __exit__ cleans up. Works with with statement.

__enter__ __exit__ Protocol methods for context managers. Enter returns resource, exit cleans up.

Decorator-based context manager

Simpler syntax with @contextmanager.

contextmanager_decorator.py
# contextmanager decorator

from contextlib import contextmanager

def main():
    # Simple context manager with decorator
    @contextmanager
    def managed_resource(name):
        print(f"  Setup: {name}")
        yield name  # Provide value to 'as' clause
        print(f"  Cleanup: {name}")
    
    print("Decorator-based context manager:\n")
    
    with managed_resource("Resource1") as res:
        print(f"  Using: {res}")
    
    
    # Context manager with exception handling
    @contextmanager
    def safe_operation(name):
        print(f"  Starting: {name}")
        try:
            yield name
        except Exception as e:
            print(f"  Error in {name}: {e}")
            raise  # Re-raise
        finally:
            print(f"  Finishing: {name}")
    
    print("\nWith exception handling:")
    
    with safe_operation("Operation1"):
        print("  Working...")
    
    print()
    
    try:
        with safe_operation("Operation2"):
            print("  Working...")
            raise ValueError("Something went wrong")
    except ValueError:
        print("  Exception caught in main")
    
    # Timer context manager
    import time
    
    @contextmanager
    def timer(label):
        start = time.time()
        yield
        elapsed = time.time() - start
        print(f"  {label}: {elapsed:.4f}s")
    
    print("\nTiming:")
    
    with timer("Computation"):
        total = sum(range(100000))
        print(f"  Sum: {total}")
    
    # Temporary directory simulation
    @contextmanager
    def temp_directory(name):
        print(f"  Creating temp dir: {name}")
        dir_path = f"/tmp/{name}"
        try:
            yield dir_path
        finally:
            print(f"  Removing temp dir: {name}")
    
    print("\nTemporary directory:")
    
    with temp_directory("work_dir") as path:
        print(f"  Using directory: {path}")
        # Do work in temp directory
    
    # Database transaction simulation
    @contextmanager
    def transaction(db_name):
        print(f"  BEGIN TRANSACTION on {db_name}")
        try:
            yield
            print(f"  COMMIT on {db_name}")
        except Exception as e:
            print(f"  ROLLBACK on {db_name} ({e})")
            raise
    
    print("\nDatabase transactions:")
    
    with transaction("users_db"):
        print("  INSERT INTO users...")
        print("  UPDATE users...")
    
    print()
    
    try:
        with transaction("orders_db"):
            print("  INSERT INTO orders...")
            raise RuntimeError("Constraint violation")
    except RuntimeError:
        print("  Transaction rolled back")
    
    # Changing context
    @contextmanager
    def working_directory(path):
        original = "/current/dir"
        print(f"  Changing to: {path}")
        try:
            yield path
        finally:
            print(f"  Restoring to: {original}")
    
    print("\nWorking directory:")
    
    with working_directory("/new/path") as path:
        print(f"  Working in: {path}")

if __name__ == "__main__":
    main()

yield separates setup from cleanup. Code before yield runs on enter.

@contextmanager `from contextlib import contextmanager`. Decorator creates context manager from generator.

Multiple contexts

Manage several resources at once.

multiple_contexts.py
# Multiple context managers

from contextlib import contextmanager

def main():
    # Nested with statements (old style)
    @contextmanager
    def resource(name):
        print(f"  Open: {name}")
        yield name
        print(f"  Close: {name}")
    
    print("Nested contexts (old style):\n")
    
    with resource("Resource1"):
        with resource("Resource2"):
            with resource("Resource3"):
                print("  Using all resources")
    
    # Multiple contexts in one with (Python 3.1+)
    print("\nMultiple in one with:")
    
    with resource("R1"), resource("R2"), resource("R3"):
        print("  Using all resources")
    
    
    # File copy simulation
    @contextmanager
    def fake_file(name, mode):
        print(f"  Opening {name} ({mode})")
        yield f"<{name} handle>"
        print(f"  Closing {name}")
    
    print("\nFile copy:")
    
    with fake_file("input.txt", "r") as src, fake_file("output.txt", "w") as dst:
        print(f"  Reading from {src}")
        print(f"  Writing to {dst}")
    
    # Database connection and cursor
    @contextmanager
    def connection(db_name):
        print(f"  Connect to {db_name}")
        yield f"<{db_name} connection>"
        print(f"  Disconnect from {db_name}")
    
    @contextmanager
    def cursor(conn):
        print(f"  Create cursor on {conn}")
        yield f"<cursor>"
        print(f"  Close cursor")
    
    print("\nDatabase operations:")
    
    with connection("users_db") as conn:
        with cursor(conn) as cur:
            print(f"  Execute query with {cur}")
    
    # Cleanup order matters
    class OrderedResource:
        def __init__(self, name, order):
            self.name = name
            self.order = order
        
        def __enter__(self):
            print(f"  [{self.order}] Enter: {self.name}")
            return self
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            print(f"  [{self.order}] Exit: {self.name}")
            return False
    
    print("\nCleanup order:")
    
    with OrderedResource("First", 1), \
         OrderedResource("Second", 2), \
         OrderedResource("Third", 3):
        print("  [X] All acquired")
    
    # Exception during acquisition
    @contextmanager
    def failing_resource(name, should_fail=False):
        print(f"  Acquiring {name}")
        if should_fail:
            raise RuntimeError(f"{name} failed to acquire")
        yield name
        print(f"  Releasing {name}")
    
    print("\nException during acquisition:")
    
    try:
        with failing_resource("R1"), \
             failing_resource("R2", should_fail=True), \
             failing_resource("R3"):
            print("  Won't reach here")
    except RuntimeError as e:
        print(f"  Error: {e}")
        print("  Note: R1 was released, R3 never acquired")
    
    # Nested transactions
    @contextmanager
    def savepoint(name):
        print(f"  SAVEPOINT {name}")
        try:
            yield
            print(f"  RELEASE SAVEPOINT {name}")
        except Exception as e:
            print(f"  ROLLBACK TO SAVEPOINT {name}")
            raise
    
    print("\nNested transactions:")
    
    try:
        with savepoint("sp1"):
            print("  Operation 1")
            with savepoint("sp2"):
                print("  Operation 2")
                raise ValueError("Error in sp2")
    except ValueError:
        print("  Rolled back to sp1")

if __name__ == "__main__":
    main()

with open(a) as f1, open(b) as f2: - all cleaned up properly.

contextlib utilities

Helpful functions for common patterns.

contextlib_utilities.py
# contextlib utilities

from contextlib import contextmanager, suppress, redirect_stdout, closing
import io

def main():
    # suppress - ignore specific exceptions
    print("Using suppress:\n")
    
    from contextlib import suppress
    
    # Without suppress
    try:
        int("not a number")
    except ValueError:
        pass  # Silently ignore
    print("  Attempt 1: Failed silently")
    
    # With suppress
    with suppress(ValueError):
        int("not a number")
    print("  Attempt 2: Failed silently")
    
    # Suppress multiple exception types
    with suppress(ValueError, TypeError, KeyError):
        d = {}
        value = d["missing_key"]
    print("  Attempt 3: Suppressed KeyError")
    
    
    # redirect_stdout - capture print output
    print("\nRedirecting stdout:")
    
    output = io.StringIO()
    
    with redirect_stdout(output):
        print("This goes to StringIO")
        print("Not to console")
    
    captured = output.getvalue()
    print(f"  Captured: {repr(captured)}")
    
    # closing - ensure object is closed
    class Resource:
        def __init__(self, name):
            self.name = name
            self.closed = False
        
        def close(self):
            print(f"  Closing {self.name}")
            self.closed = True
    
    print("\nUsing closing:")
    
    with closing(Resource("res1")) as res:
        print(f"  Using {res.name}")
    print(f"  Closed: {res.closed}")
    
    # nullcontext - conditional context manager
    from contextlib import nullcontext
    
    print("\nConditional context:")
    
    use_lock = False
    
    @contextmanager
    def lock():
        print("  Acquiring lock")
        yield
        print("  Releasing lock")
    
    context = lock() if use_lock else nullcontext()
    
    with context:
        print("  Critical section (no lock)")
    
    use_lock = True
    context = lock() if use_lock else nullcontext()
    
    with context:
        print("  Critical section (with lock)")
    
    # ExitStack - dynamic context managers
    from contextlib import ExitStack
    
    print("\nExitStack:")
    
    @contextmanager
    def file_context(name):
        print(f"  Open {name}")
        yield name
        print(f"  Close {name}")
    
    files = ["file1.txt", "file2.txt", "file3.txt"]
    
    with ExitStack() as stack:
        handles = [stack.enter_context(file_context(f)) for f in files]
        print(f"  All files opened: {handles}")
    
    print("  All files closed")
    
    # ExitStack with callbacks
    print("\nExitStack with callbacks:")
    
    with ExitStack() as stack:
        stack.callback(lambda: print("  Callback 1"))
        stack.callback(lambda: print("  Callback 2"))
        print("  In context")
    
    # Practical: resource pool
    print("\nResource pool:")
    
    def acquire_resources(count):
        stack = ExitStack()
        resources = []
        try:
            for i in range(count):
                res = Resource(f"res{i}")
                stack.enter_context(closing(res))
                resources.append(res)
            return stack.pop_all(), resources
        except:
            stack.close()
            raise
    
    with ExitStack() as stack:
        mgr, resources = acquire_resources(3)
        stack.enter_context(mgr)
        print(f"  Using {len(resources)} resources")

if __name__ == "__main__":
    main()

suppress(), redirect_stdout(), closing() - ready-made context managers.

Exercise: practical.py

Build a database connection context manager