C.W.K.
Stream
Lesson 05 of 06 · published

contextlib — Generators as Context Managers

~18 min · contextlib, contextmanager, generator, suppress

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

contextlib.contextmanager — the easier way

Writing a class with __enter__/__exit__ is verbose for simple cases. contextlib.contextmanager turns a generator into a context manager. Code before the yield is the setup. The yielded value is what's bound by as. Code after the yield is the teardown. Exceptions from the body propagate through the yield, so wrap it in try/finally to guarantee cleanup.

The shape — one yield, with try/finally for safety

The canonical shape: setup, then try: yield value; finally: teardown. The try ensures teardown runs even if the body raises. Without it, an exception in the body skips the teardown — exactly the kind of bug context managers were invented to prevent.

contextlib.suppress — the swallowing pattern made cleanly

with contextlib.suppress(FileNotFoundError): swallows that exception type and exits the block. Cleaner than try/except/pass for the "ignore this specific error" pattern, especially in non-trivial cleanup paths.

contextlib.ExitStack — composing many contexts dynamically

Sometimes you don't know how many contexts to enter at compile time — you have a list of files to open, a variable number of locks. ExitStack is a context manager that lets you push other contexts onto a stack as you go, and unwinds them all on exit. It replaces the only-deep-nested workaround.

Pythonic Way: Reach for @contextmanager over class-based context managers when the setup/teardown is simple enough to fit in a generator. Reach for the class form when you need state that survives across __enter__ calls or want a richer interface (re-entrancy, configuration).

Code

@contextmanager — generator as context manager·python
import contextlib
import time

@contextlib.contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield                              # control returns to the with block
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed*1000:.2f}ms")

with timer("sum million"):
    sum(range(1_000_000))
# sum million: 12.34ms
@contextmanager with a yielded value·python
import contextlib

@contextlib.contextmanager
def temp_setting(d, key, value):
    """Temporarily set d[key]=value, restore on exit."""
    original = d.get(key)
    had_key = key in d
    d[key] = value
    try:
        yield d                            # bound by `as`
    finally:
        if had_key:
            d[key] = original
        else:
            del d[key]

config = {"debug": False}
with temp_setting(config, "debug", True) as cfg:
    print("inside:", cfg["debug"])         # inside: True
print("outside:", config.get("debug"))     # outside: False
contextlib.suppress — clean swallow·python
import contextlib
import os

# Old way — verbose
try:
    os.remove("maybe.txt")
except FileNotFoundError:
    pass

# New way — declarative
with contextlib.suppress(FileNotFoundError):
    os.remove("maybe.txt")

# Multiple types
with contextlib.suppress(FileNotFoundError, PermissionError):
    os.remove("/protected/path.txt")
contextlib.ExitStack — dynamic composition·python
import contextlib
import io

# Open N files (N known only at runtime)
file_specs = ["a", "b", "c"]            # N items
fake_files = {name: io.StringIO(f"contents of {name}") for name in file_specs}

with contextlib.ExitStack() as stack:
    files = [stack.enter_context(fake_files[n]) for n in file_specs]
    # all files now open; will all close cleanly on exit
    for f, name in zip(files, file_specs):
        print(name, f.read())
contextlib.closing — anything with .close()·python
import contextlib
import urllib.request

# urlopen returns something with .close() but isn't a context manager pre-3.0
with contextlib.closing(urllib.request.urlopen("https://example.com")) as r:
    data = r.read()
# r.close() is called automatically

# Useful for wrapping any resource that has .close() but isn't a CM

External links

Exercise

Build (a) a @contextmanager called silenced(streams=('stdout',)) that redirects stdout (and optionally stderr) to a StringIO during the block and prints what was captured on exit. (b) A version using contextlib.ExitStack that opens all paths in paths: list[str], reads each file, and ensures all files close. Test (a) by writing some prints inside the block. Test (b) using io.StringIO instances since you don't have real files.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.