Context Managers Encode Unwinding, Not Just Cleanup
How with statements enter, unwind, suppress exceptions, and scale to dynamic resources with ExitStack.
A context manager brackets a suite with setup and teardown, but its protocol is specifically designed around exception unwinding. __enter__ produces the value after as; __exit__ receives exception information and can suppress the active exception by returning a truthy value.
class transaction:
def __init__(self, connection):
self.connection = connection
def __enter__(self):
self.connection.begin()
return self.connection
def __exit__(self, exc_type, exc, traceback):
if exc_type is None:
self.connection.commit()
else:
self.connection.rollback()
return False
Returning False means any exception continues propagating. The return value is ignored when the suite completed normally.
Multiple managers form a stack
with open(source_path) as source, open(target_path, 'w') as target:
target.write(source.read())
Managers enter from left to right and exit from right to left. The behaviour is equivalent to nested with statements.
If entering target raises, the source manager has not yet reached a successfully established body, so its __exit__ method is skipped. Only managers for which the suite began participate in unwinding.
An inner __exit__ can suppress an exception. Outer managers then receive (None, None, None), reflecting the updated exception state rather than the original failure.
Generator-based managers need one yield
contextlib.contextmanager adapts a generator into the protocol. Code before yield enters, the yielded value is bound after as, and the remainder runs during exit.
from contextlib import contextmanager
@contextmanager
def temporary_setting(config, value):
previous = config.setting
config.setting = value
try:
yield
finally:
config.setting = previous
The generator must yield exactly once. Exceptions from the body are thrown back at the yield point, so catching an exception and not re-raising it suppresses it.
ExitStack handles a runtime-sized set
ExitStack maintains callbacks with the same last-in, first-out discipline. enter_context(manager) enters a normal manager and registers its exit. push(exit_method) registers an exit-compatible callable without entering it.
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(path)) for path in paths]
consume(files)
stack.callback(function, *args) is a convenience for ordinary callbacks. Those callbacks receive the active exception type, value, and traceback as their first three arguments, and a truthy result can suppress the exception just like __exit__.
AsyncExitStack extends the same model to asynchronous context managers and coroutine cleanup. The central design remains the same: successful acquisition immediately registers its inverse, and unwinding follows the reverse order even through partial failure.