Context managers and with

Python · Practical · 10 min

Files, locks, transactions and connections must be released even when errors happen.

The with statement pairs setup with guaranteed cleanup. with expr as x: calls expr.__enter__() (binding its result to x), runs the block, and then calls __exit__ no matter how the block ends — normally, by return, or by an exception. It replaces error-prone try/finally boilerplate.

You've met it with files. The same pattern covers threading.Lock objects, database transactions, temporary directories (tempfile.TemporaryDirectory()), and patched state in tests. Several managers can share one statement: with open(a) as src, open(b, "w") as dst: (with parentheses to span lines since 3.10).

The easiest way to write one is contextlib.contextmanager on a generator: code before yield is setup, the yielded value is bound by as, and code after yield is cleanup. Wrap the yield in try/finally, otherwise cleanup is skipped when the block raises.

The class form implements __enter__(self) and __exit__(self, exc_type, exc, tb). If __exit__ returns a truthy value the exception is suppressed — rarely what you want, so return None.

contextlib has more helpers worth knowing:

  • suppress(FileNotFoundError) ignores a specific exception
  • ExitStack manages a dynamic number of contexts
  • chdir(path) (3.11) temporarily changes directory
  • closing(obj) calls obj.close() on exit

Async code has a parallel async with statement and asynccontextmanager.

Example

import time
from contextlib import contextmanager, suppress
import os

@contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield start                    # value bound by `as`
    finally:                           # runs even if the block raises
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.3f}s")

with timer("sum"):
    total = sum(range(1_000_000))

try:
    with timer("failing"):
        1 / 0
except ZeroDivisionError:
    print("cleanup still ran")

with suppress(FileNotFoundError):
    os.remove("does-not-exist.tmp")
print("no error raised")

Remember

  • with guarantees __exit__ runs, even on exceptions
  • @contextmanager: setup, yield, cleanup in finally
  • contextlib.suppress and ExitStack cover common needs

Watch out for

  • Writing a @contextmanager without try/finally around yield, so cleanup is skipped on errors.

Check yourself

  1. In a `@contextmanager` generator, what does the value after `yield` become?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/contextlib.html