Context managers: __enter__/__exit__, contextlib.contextmanager, ExitStack
with obj: calls obj.__enter__(), binds its return value (if as x), always calls obj.__exit__(exc_type, exc_val, exc_tb) on the way out -- including when the body raises. If __exit__ returns a truthy value, the
exception is suppressed instead of propagating; returning None/False (the default) lets it continue.
class Resource:
def __enter__(self):
print(" acquiring")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" releasing (exception type seen: {exc_type})")
return False # don't suppress
with Resource() as r:
print(" using")
try:
with Resource() as r:
raise ValueError("boom")
except ValueError:
print("exception still propagated after __exit__ ran")
@contextlib.contextmanager turns a generator into a context manager: everything before yield is __enter__, everything after is __exit__.
import contextlib
@contextlib.contextmanager
def managed():
print(" setup")
yield "value"
print(" teardown")
with managed() as v:
print(" got:", v)
class Suppressor:
def __enter__(self): return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" suppressing {exc_type}")
return True # True -> swallow the exception
with Suppressor():
raise RuntimeError("this gets suppressed")
print("execution continues normally after a suppressed exception")
ExitStack handles a variable number of context managers -- useful when you don't know how many resources you need until runtime:
from contextlib import ExitStack
with ExitStack() as stack:
resources = [stack.enter_context(managed()) for _ in range(3)]
print(" got", len(resources), "resources; all three tear down on exit, in reverse order")