Mastery
Mastery/Python/D. Classes & OOP internals
T1 · high-leverage

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.

python
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__.

python
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:

python
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")

Interview angle

"Write a context manager that times a block of code" is a common practical exercise — it's small enough to finish in an interview, but touches __enter__/__exit__, exception propagation, and (for bonus points) whether the candidate reaches for @contextlib.contextmanager instead of a full class when a generator would do. Asking what __exit__'s return value means is a good follow-up most people get wrong on the first try.

In the industry

Context managers are the idiomatic way to guarantee cleanup in Python — file handles, database connections/transactions, locks, and temporary state changes (unittest.mock.patch) are all built on this protocol. Code review in any serious Python codebase treats manual acquire()/release() pairs without a try/finally or a context manager as a real bug waiting to happen, since any exception between the two leaks the resource.