Python Context Manager Protocol
A context manager implements setup and cleanup around a `with` block through `__enter__()` and `__exit__()`. Its exit hook runs during normal completion and exception unwinding, and can choose whether an exception propagates.
A context manager is an object that defines how a `with` block begins and ends. The protocol consists of `__enter__()` and `__exit__(exc_type, exc_value, traceback)`.
Python evaluates the manager expression, calls `__enter__()`, and binds its return value to the name after `as`, if present. The block then runs. When it finishes, Python calls `__exit__()` whether the block completed normally or raised an exception.
If the block raises an exception, `__exit__()` receives its type, value, and traceback. Returning a truthy value suppresses that exception; returning a false value allows it to propagate. If the block completes normally, the three exception arguments are `None`.
Context managers are useful for resources with a clear lifetime, such as files, locks, and database transactions. They centralize cleanup so callers do not have to repeat a `try/finally` pattern. The standard library's `contextlib` provides helpers, including `contextmanager`, for defining managers with less boilerplate.
Key points
- The context manager protocol consists of `__enter__()` and `__exit__()`.
- `__enter__()` may return a value for the optional `as` target.
- `__exit__()` runs when the suite ends, including during exception unwinding.
- Returning a truthy value from `__exit__()` suppresses the suite's active exception.
- `contextlib.contextmanager` can build a manager from a generator.
Interview questions
What is a context manager in Python?
A context manager defines behavior for entering and leaving a `with` block. Its `__enter__()` method performs setup and returns the value optionally bound by `as`. Its `__exit__()` method handles cleanup and is called even if the block raises. It can suppress an exception by returning a truthy value.
What is the execution order of a `with` statement?
Python calls `__enter__()` before the suite and uses its return value for the optional `as` target. After the suite, it calls `__exit__()` with three `None` values if the suite completed normally. If the suite raised, it passes the exception type, exception instance, and traceback. A truthy result suppresses the exception; a false result lets it propagate.
How should `__exit__()` decide whether to suppress an exception?
Return `False` or another false value from `__exit__()` when the exception should continue outward. Return a truthy value only when the manager deliberately handles the exception and the caller should continue as if it were suppressed. Suppression is limited to the exception from the managed suite; exceptions raised by `__exit__()` itself are not suppressed by that same call. In most resource managers, cleanup should happen and the original exception should propagate.
How does the protocol help manage resource lifetimes safely?
`__enter__()` performs setup and returns the object or resource that the block needs. `__exit__()` releases or finalizes it, regardless of how the block ends. If setup in `__enter__()` fails, the suite was never entered and Python does not call that manager's `__exit__()`. Therefore, `__enter__()` should avoid leaving partially acquired resources behind if it can fail partway through.
How does `contextlib.contextmanager` implement a context manager?
`contextlib.contextmanager` lets a generator describe a context manager. Code before `yield` runs on entry, the yielded value becomes the `as` target, and code after `yield` runs during exit. If the suite raises, that exception is thrown at the `yield` point, so a `try/finally` is a common way to guarantee cleanup. To suppress an exception, the generator must handle it rather than re-raise it.