Context Managers: with Done Right
Guarantee cleanup with __enter__/__exit__ and @contextmanager.
with guarantees cleanup even when an exception flies through. You use
it daily with files โ now build your own:
class Timer:
def __enter__(self):
self.start = time.monotonic()
return self # bound to the as-variable
def __exit__(self, exc_type, exc, tb):
self.elapsed = time.monotonic() - self.start
return False # do not swallow exceptions
with Timer() as t:
do_work()
print(t.elapsed)
__exit__ receives the exception details (or None). Returning True
suppresses the exception; returning False (the usual choice) lets it
propagate. That decision is the whole error-handling story of context
managers: cleanup always runs; failures stay visible.
The lightweight way: @contextmanager
For simple cases, a generator with @contextmanager reads better than a
class:
from contextlib import contextmanager
@contextmanager
def indent_log(depth):
print(" " * depth + ">")
yield # everything before = __enter__, after = __exit__
print(" " * depth + "<")
Why not try/finally?
with is try/finally with a name and a protocol. The advantage is
reusability: write the cleanup once, use it at fifty call sites, and nothing
forgets it. Resource discipline โ connections, locks, temp files โ should be
invisible to callers, and context managers are the tool that makes it so.