Chapter 6 · Senior Developer

Context managers (with)

A context manager is the machinery behind with open(...) as f:. The with block guarantees that some setup runs at the start and some teardown runs at the end — even if an exception is raised in the middle. That's the whole point: cleanup you can't forget and can't skip.

The protocol is two methods. __enter__ runs when you enter the block and its return value is bound to the as name. __exit__ runs when you leave — normally OR because of an error. Python calls __exit__(exc_type, exc_val, exc_tb) with the exception details (or three Nones if all went well). If __exit__ returns a truthy value it *swallows* the exception; return False (or nothing) to let it propagate.

Writing a full class is overkill for simple cases, so contextlib.contextmanager lets you write one as a generator: everything before the single yield is the setup, the yield hands control to the with body, and everything after is the teardown. Wrap the yield in try/finally if the cleanup must run on errors too.

Use context managers for anything that's acquired-then-released: files, database connections, locks, temporary directories, timers, or any "do X, then guarantee un-X".

Syntax

# Class-based:
class Managed:
    def __enter__(self):
        # setup; return value goes to `as`
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        # teardown — always runs
        return False                # False = don't suppress exceptions

with Managed() as m: ...

# Generator-based (contextlib):
import contextlib
@contextlib.contextmanager
def managed():
    # setup
    yield resource
    # teardown

Worked examples

Cleanup runs even when the block raises

log = []

class Resource:
    def __enter__(self):
        log.append("open")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        log.append("close")        # runs no matter what
        return False               # let the error propagate

try:
    with Resource():
        log.append("work")
        raise RuntimeError("boom")
except RuntimeError:
    log.append("caught")

print(log)
# Output:
# ['open', 'work', 'close', 'caught']

A generator-based timer with @contextlib.contextmanager

import contextlib

@contextlib.contextmanager
def timer(label):
    print(f"start {label}")    # setup (before yield)
    yield                      # the with-body runs here
    print(f"end {label}")      # teardown (after yield)

with timer("job"):
    print("running")
# Output:
# start job
# running
# end job

Suppressing chosen exceptions, propagating the rest

import contextlib

@contextlib.contextmanager
def suppress(*types):
    try:
        yield
    except Exception as e:
        if not isinstance(e, types):
            raise              # not in the list — re-raise

with suppress(ValueError):
    raise ValueError("silenced")
print("survived")
# Output:
# survived

Common mistakes & gotchas

__exit__ must accept three exception arguments

Even when nothing goes wrong, Python calls __exit__(self, exc_type, exc_val, exc_tb) with three values (all None on success). Defining def __exit__(self): raises TypeError. Use def __exit__(self, exc_type, exc_val, exc_tb): or def __exit__(self, *exc):.

Returning True from __exit__ silently eats errors

If __exit__ returns a truthy value, the exception inside the with block is suppressed and your program carries on as if nothing happened — a great way to hide bugs. Return False (or nothing, which is None/falsy) unless you genuinely mean to swallow the error.

Generator context managers need try/finally for guaranteed cleanup

In an @contextmanager, if the with body raises, the exception is thrown *at* the yield — so code plainly written after yield is skipped. If the teardown must always run (closing a file, releasing a lock), wrap it: try: yield / finally: cleanup().

Why it matters

Resource leaks — unclosed files, dangling connections, locks never released — are some of the nastiest production bugs because they show up only under load. Context managers make correct cleanup the default and the readable choice, which is why `with` is everywhere in mature Python code.