@contextlib.contextmanager turns a generator into a context manager. Code
before the yield is __enter__, code after it is __exit__, and the
yielded value is what as binds.
@contextmanager
def temp_attr(obj: object, name: str, value: object) -> Iterator[None]:
original = getattr(obj, name, _MISSING)
setattr(obj, name, value)
try:
yield
finally:
if original is _MISSING:
delattr(obj, name)
else:
setattr(obj, name, original)
The try/finally is the whole thing
Without it:
@contextmanager
def temp_attr(obj, name, value):
original = getattr(obj, name, None)
setattr(obj, name, value)
yield
setattr(obj, name, original) # only reached on the happy path
When the body raises, contextlib throws that exception into the
generator at the yield. With no try, the generator propagates it
immediately and the restore line never executes. The attribute stays patched
for the rest of the process.
This is the exact scenario a context manager exists to handle, so a version that only cleans up when nothing went wrong is worse than no context manager at all — it looks like it guarantees something it does not, and it passes every test that does not deliberately raise inside the block.
Use finally, not except. except requires you to re-raise and it is easy
to swallow by accident; finally cleans up and lets the exception continue
outward, which is almost always right.
💡A @contextmanager generator that swallows the exception — a
click to reveal
bare except Exception: pass around the yield — suppresses it. What is the
equivalent for a class-based context manager, and why is the generator version
more dangerous?
For a class, __exit__ suppresses by returning a truthy value. That is an
explicit, greppable decision on one line, and the signature
-> bool documents that suppression is possible.
In a generator, suppression happens by not re-raising — an absence rather
than a presence. try: yield / except SomeError: pass looks like ordinary
error handling and silently makes with swallow exceptions from the caller’s
body. Reviewers read it as “handle this error”, not “the block’s exceptions
now vanish”.
The related trap: a return inside the except has the same effect. If you
genuinely mean to suppress, say so — contextlib.suppress exists, and its
name is the documentation.
They are single-use
The object returned by calling a @contextmanager function wraps one
generator instance. Entering it twice fails:
cm = temp_attr(obj, "mode", "test")
with cm: pass
with cm: pass # raises
On CPython 3.12-3.14 that second __enter__ raises AttributeError, because
__enter__ deletes the stored constructor arguments as it runs. The exact
exception type is an implementation detail; the contract is “single-use”.
What is safe to reuse is the function: calling temp_attr(...) again
builds a fresh generator. And using a @contextmanager function as a
decorator (supported since 3.2) is safe for the same reason — it creates a
new context manager per call, not per decoration.
⚠ 3.15 changes ContextDecorator to detect generator and coroutine
functions and keep the context manager open across iteration or await. That is
a real behaviour change for a decorated generator function: today the manager
closes when the generator is created, and on 3.15 it stays open until the
generator is exhausted. If you decorate generators, test on both.
Annotate the return Iterator[T], not T
The most common mistake in the whole module:
@contextmanager
def opened(path: str) -> TextIO: # WRONG
...
yield handle
The generator function returns an iterator that yields TextIO. Its
annotation is Iterator[TextIO] (or Generator[TextIO, None, None]); the
decorator is what turns that into something whose __enter__ gives a
TextIO. Annotating it -> TextIO is a straightforward lie and --strict
catches it.
💡When should you write a class with __enter__/__exit__ instead
click to reveal
of using @contextmanager?
Four situations.
When it must be reusable or reentrant. The generator version is single-use by construction. A lock-like object you enter repeatedly, or nest inside itself, needs a class.
When it needs to inspect the exception. __exit__ receives
(exc_type, exc_value, traceback) as ordinary arguments, which is far more
readable than the generator equivalent (except-ing around the yield and
re-raising).
When it is also a normal object. A database connection that supports
with conn: and also has thirty methods is a class that happens to implement
the protocol, not a context manager that happens to have methods.
When suppression is part of the contract. Returning True from __exit__
is explicit; not-re-raising in a generator is not.
For everything else — and that is most cases: acquire, yield, release —
@contextmanager is shorter, and the try/finally makes the pairing visually
obvious in a way that two separate methods do not.