Context Managers

Difficulty: Advanced

Context managers in Python provide a clean way to manage resources that need setup and teardown, such as database connections, locks, or temporary state changes. The `with` statement ensures that cleanup code always runs, even if an exception occurs inside the block.

The context manager protocol requires two methods: __enter__() and __exit__(). When a `with` block begins, Python calls __enter__() on the context manager, and the return value is bound to the variable after `as`. When the block ends (normally or due to an exception), Python calls __exit__(), which receives exception information as arguments. If __exit__ returns True, the exception is suppressed; otherwise it propagates.

For simpler cases, the contextlib module provides the @contextmanager decorator, which lets you write a context manager as a generator function with a single yield. Code before yield runs as __enter__, the yielded value is bound to the `as` variable, and code after yield runs as __exit__. This avoids the boilerplate of writing a full class.

Context managers are used throughout Python's standard library and popular frameworks. The open() function is the most common example, but they also appear in threading (locks), decimal (local precision), and database libraries (transactions). Understanding context managers is essential for writing robust, resource-safe Python code.

You can also nest context managers or use contextlib.ExitStack to manage a dynamic number of context managers. These advanced patterns are useful when the number of resources to manage is not known until runtime.

Code examples

Class-Based Context Manager

class ManagedResource:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print(f"Acquiring {self.name}")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Releasing {self.name}")
        return False

with ManagedResource("database") as res:
    print(f"Using {res.name}")

__enter__ is called when entering the with block and returns self (bound to 'res'). __exit__ is called when leaving the block, ensuring cleanup always happens.

Context Manager with Exception Handling

class SafeBlock:
    def __enter__(self):
        print("Entering safe block")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            print(f"Caught exception: {exc_val}")
        print("Exiting safe block")
        return True

with SafeBlock():
    print("Inside block")
    raise ValueError("something went wrong")

print("After block")

Returning True from __exit__ suppresses the exception. Without return True, the ValueError would propagate after __exit__ runs.

Using contextlib.contextmanager

from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f"<{name}>")
    yield name
    print(f"</{name}>")

with tag("div") as t:
    print(f"Content inside {t}")

The @contextmanager decorator turns a generator into a context manager. Code before yield is __enter__, the yielded value is the 'as' variable, and code after yield is __exit__.

Nested Context Managers

class Indent:
    level = 0

    def __init__(self, label):
        self.label = label

    def __enter__(self):
        Indent.level += 1
        print("  " * Indent.level + f"Enter {self.label}")
        return self

    def __exit__(self, *args):
        print("  " * Indent.level + f"Exit {self.label}")
        Indent.level -= 1
        return False

with Indent("outer"):
    with Indent("inner"):
        print("  " * Indent.level + "Deep inside")

Each nested with block calls __enter__ on entry and __exit__ on exit. The class-level 'level' variable tracks nesting depth for proper indentation.

Key points

Concepts covered

with statement, __enter__, __exit__, contextlib.contextmanager