Python/Advanced Python/Context Managers & with

Context Managers & with

A context manager pairs setup with guaranteed teardown: the `with` statement runs `__enter__` before a block and `__exit__` after it, so resources like files, locks and transactions are always released — even when the block raises.

Advanced30mInterview Production

Learning objectives

  • Explain how `with` guarantees resource cleanup on every exit path, including exceptions
  • Implement the `__enter__`/`__exit__` protocol and control exception suppression via `__exit__`'s return value
  • Write concise generator-based managers with `contextlib.contextmanager`
  • Compose managers dynamically with `ExitStack` and ignore chosen errors with `suppress`

Prerequisites

Exceptions & try/finallyClasses & dunder methodsGenerators

Version notes

  • · The `with` statement (PEP 343) and `contextlib.contextmanager` have existed since Python 2.5.
  • · Comma-separated managers in one `with` work throughout Python 3; the parenthesised multi-line form was made official in 3.10 via the PEG parser.
  • · `contextlib.ExitStack` arrived in 3.3; `contextlib.suppress` in 3.4.
  • · Async context managers (`async with`, `__aenter__`/`__aexit__`) require Python 3.5+; `contextlib.AsyncExitStack` arrived in 3.7.

Simple explanation

What it is. A context manager is any object implementing `__enter__` and `__exit__`. The `with` statement uses them to run setup before a block and guaranteed teardown after it — no matter how the block ends: normal completion, return, break, or an exception.

Why it exists. Resources such as files, sockets, locks and database transactions must be released deterministically. Leaning on `del`/garbage collection is unreliable in timing and can leak handles, while sprinkling try/finally everywhere is verbose and easy to get wrong under refactoring.

The problem it solves. It ties cleanup to a lexical block so acquire and release can never be mismatched, runs correctly on every exit path, and packages the boilerplate once so the call sites stay short and obviously correct.

How it works. `with cm as x:` calls `cm.__enter__()` and binds its return value to `x`, runs the body, then always calls `cm.__exit__(exc_type, exc, tb)`. If the body raised, the exception triple is passed in; `__exit__` returning a truthy value suppresses it, falsy (the default) lets it propagate after cleanup. `@contextmanager` lets you write a manager as a generator where the single `yield` marks the boundary between setup and teardown.

Analogy: Like a rental-car counter. `__enter__` hands you the keys (acquire), you drive around (the block), and `__exit__` is the drop-off desk that always files the paperwork and charges you — whether you bring the car back spotless or wrap it around a lamppost.

Common misconceptions

  • "`with` catches exceptions." — By default it does not. `__exit__` runs the cleanup and then the exception keeps propagating unless `__exit__` explicitly returns True.
  • "`with open(...) as f` binds `f` to the file because `open` returns a file." — `f` is bound to whatever `__enter__` returns, not to the manager. For files they happen to be the same object, but that is not true in general.
  • "A `@contextmanager` generator can `yield` several times." — It must yield exactly once; yielding a second time raises RuntimeError.
  • "You need a class to write a context manager." — `@contextmanager` on a generator is usually shorter and clearer for a plain setup/teardown pair.

Common alternatives

  • Plain try/finally (what `with` compiles down to) when a reusable manager is overkill
  • `contextlib.ExitStack` for a dynamic or unknown number of managers
  • `contextlib.closing` to adapt objects that only expose a `.close()` method
  • `contextlib.suppress` to ignore specific exceptions concisely

Interactive visualization

Step 1 / 6
1class Managed:
2 def __enter__(self):
3 print("enter")
4 return self
5 def __exit__(self, *exc):
6 print("exit")
7
8with Managed() as m:
9 print("body")
10 raise ValueError("boom")

Call stack

<module>

Managed.__enter__

Output

enter

with calls __enter__

Entering the with-block builds a Managed() and calls __enter__ to acquire the resource.

Code examples

BeginnerThe __enter__/__exit__ protocol
python
Loading editor…

Note: `__enter__`'s return value is what `as` binds. `__exit__` receives the exception triple (or three Nones on success) and returns falsy to re-raise, truthy to suppress.

Behind the scenes

  • `with cm as x:` desugars to: look up `type(cm).__enter__` and `type(cm).__exit__` on the type (not the instance), call `__enter__`, bind the result to `x`, run the body, then always call `__exit__`.
  • `__exit__(exc_type, exc, tb)` gets the exception triple (or three Nones on success). Returning truthy suppresses the exception; returning falsy — including an implicit None — re-raises it, which is why forgetting to `return` correctly propagates errors.
  • `with A() as a, B() as b:` is exactly nested `with` blocks: B is entered after A, and on the way out `B.__exit__` runs before `A.__exit__` (LIFO), so B is torn down first even if A's teardown later fails.
  • `@contextmanager` wraps a generator: `__enter__` calls `next(gen)` to run up to the `yield` and returns the yielded value; `__exit__` calls `gen.throw(...)` on error or `next(gen)` on success to run the code after the yield.
  • `contextlib.ExitStack` enters a variable number of managers at runtime and unwinds them in LIFO order; `contextlib.suppress(*excs)` is a tiny manager whose `__exit__` simply returns True for the listed exception types.
  • Async context managers implement `__aenter__`/`__aexit__` (both coroutines) and are driven by `async with`, which awaits each — used for pooled connections, async locks and HTTP client sessions.

Interview insights

Key definitions

  • · Context manager: an object implementing `__enter__` and `__exit__`, usable in a `with` statement to bracket a block with setup and guaranteed teardown.
  • · `__exit__` return value: truthy suppresses the exception raised in the block; falsy (the default) lets it propagate after cleanup has run.
How does a `with` statement guarantee cleanup, and what happens if the block raises?

Concise answer: `with` calls `__enter__` before the block and always calls `__exit__` after it — on normal exit or on an exception. If the block raises, the exception triple is passed to `__exit__`; unless it returns True, the exception propagates after cleanup.

Detailed answer: The statement is compiled to the equivalent of try/finally around the body, with `__exit__` in the finally-like position, so cleanup runs on every exit path (return, break, or exception). On an exception, `__exit__(exc_type, exc, tb)` receives the details and can inspect or log them; returning a truthy value tells Python to swallow the exception, while returning falsy (or nothing) re-raises it once cleanup is done.

Common mistake: Assuming `with` catches exceptions. By default it only guarantees cleanup; the error still propagates.

Follow-ups:

  • · How would you make a manager suppress a specific exception?
  • · What order do multiple managers unwind in?
When would you use `contextlib.contextmanager` instead of a class, and how does the generator form work?

Concise answer: Use the decorator for a simple setup/teardown pair — it is shorter than writing `__enter__`/`__exit__`. Code before the single `yield` is the setup, the yielded value is bound to `as`, and code after `yield` (ideally in a `finally`) is the teardown.

Detailed answer: `@contextmanager` turns a generator into a context manager. `__enter__` runs the generator up to `yield` and returns the yielded value; `__exit__` resumes it — throwing the exception in at the yield point if the body raised — so a try/finally around the yield gives you cleanup that runs on both success and failure. Prefer a class when you need to store rich state, reuse the object, or support the async protocol explicitly.

Common mistake: Putting cleanup after `yield` but not in a `finally`, so it is skipped when the body raises.

Follow-ups:

  • · What happens if the generator yields twice?
  • · How do you write the async version?
What is `ExitStack` for, and how do you ignore a specific exception from a block?

Concise answer: `contextlib.ExitStack` lets you enter a dynamic number of context managers and guarantees they all unwind in LIFO order. `contextlib.suppress(Err)` ignores just the listed exception types by returning True from its `__exit__`.

Detailed answer: When the number of resources is only known at runtime (a list of files, N connections), you cannot hard-code commas in a `with`. `ExitStack` collects managers with `stack.enter_context(cm)` and closes every registered one when the stack exits, even if an earlier close fails. `suppress` is the concise replacement for a `try/except SomeError: pass` — but keep its body small so you do not accidentally swallow unrelated errors.

Common mistake: Wrapping a large block in `suppress`, which hides errors you did not intend to ignore.

Follow-ups:

  • · How does `ExitStack` differ from nesting many `with` blocks?
  • · Is there an async ExitStack?

Knowledge check

What's the output?Question 1 of 3

What does this program print?

python
class C:
    def __enter__(self): return self
    def __exit__(self, *a):
        print('exit'); return True
with C():
    raise ValueError('x')
print('after')
Multiple choiceQuestion 2 of 3

In `with cm as x:`, what is `x` bound to?

Multiple choiceQuestion 3 of 3

You must open a number of files known only at runtime and ensure every one is closed. Which tool fits best?

Score: 0/3