Context Managers
Interview answer (say this first). A context manager is an object that guarantees setup before a block of code runs and cleanup after it finishes — even if the block raises an exception. The
withstatement drives it, using__enter__to set up and__exit__to tear down.
Why this exists
Almost every useful resource needs releasing: files, database connections, network sockets, locks, and temporary directories. Releasing them correctly by hand is easy to get wrong.
f = open("data.csv")
rows = f.readlines()
process(rows)
f.close() # if process() raises, this line never runs
If process(rows) throws, f.close() is skipped and the file handle leaks. In a long-running service the process eventually runs out of file descriptors and starts failing everything, long after the original mistake.
The manual fix is a try/finally:
f = open("data.csv")
try:
rows = f.readlines()
process(rows)
finally:
f.close() # always runs
This is correct but verbose, and it is repeated around every resource. It is also easy to forget one of the steps when the setup is more involved, such as opening a connection, starting a transaction, and acquiring a lock.
Worse, some resources are only safe with a guarantee. A database transaction that is not rolled back on failure leaves locks held and connections poisoned. A lock that is not released deadlocks the service. A temporary directory that is not removed fills the disk. These are not close() calls you can remember; they are correctness requirements.
Context managers exist to make correct cleanup the default and forgetting it hard.
Start from zero
| Word | Plain meaning |
|---|---|
| Resource | Something acquired and later released: a file, socket, lock, transaction, connection. |
| Setup / teardown | Code that runs before and after a block, also called acquire and release. |
with statement | The syntax that runs setup, the block, and teardown in a guaranteed order. |
__enter__ | The method called when the block starts. Its return value is what as binds. |
__exit__ | The method called when the block ends, normally or with an exception. |
| Context manager | Any object that implements __enter__ and __exit__ (the context manager protocol). |
| Suppression | Returning a truthy value from __exit__ to stop an exception from propagating. |
| Traceback | The report of the exception’s type, value, and call chain. |
Two distinctions matter.
A context manager is not the same as a resource. The resource is the file or connection. The context manager is the object that knows how to open and close it. open("f") returns a file object, which happens to also be a context manager — that is why with open(...) works.
Setup always runs; teardown always runs. That guarantee is the entire value. The with block cannot skip __exit__, even on return, break, continue, or an exception.
The with statement is sugar you could write yourself:
cm = SomeContextManager()
value = cm.__enter__()
try:
# the with-block body
...
except BaseException as exc:
if not cm.__exit__(type(exc), exc, exc.__traceback__):
raise
else:
cm.__exit__(None, None, None)
Everything in this topic follows from that expansion.
The core idea
Think of a hospital operating room. A patient is prepared, the procedure happens, and a cleanup protocol runs afterward no matter how the procedure goes. Nobody says “if the surgery goes well, remember to sterilise.” The cleanup is part of the room, not part of the surgeon’s memory.
A context manager is that protocol attached to the block:
The
withstatement is a promise that cleanup happens.
flowchart LR
A["with cm as x:"] --> B["__enter__() → x"]
B --> C["body runs"]
C -->|"normal"| D["__exit__(None, None, None)"]
C -->|"exception"| E["__exit__(type, value, tb)"]
E -->|"returns truthy"| F["exception suppressed"]
E -->|"returns falsy"| G["exception propagates"]
The elegant part is the exception path. __exit__ is handed the exception details before the error escapes, which lets it decide: clean up and re-raise (the normal case), or handle the exception and suppress it.
How it works
- Python evaluates the expression after
withto get the context manager object. - It calls
__enter__(). The value returned is bound to the name afteras. Many managers returnself;open()returns the file. - The block body runs.
- If the body finishes normally,
__exit__is called with threeNonearguments. - If the body raises,
__exit__is called with the exception type, value, and traceback. __exit__returns a value. A falsy return (includingNone) lets the exception propagate. A truthy return suppresses it, and execution continues after thewithblock.__exit__runs on every exit path — normal completion, exception,return,break, andcontinue. This is the guarantee.contextlib.contextmanagerbuilds a context manager from a generator. Code beforeyieldis the setup, the yielded value is whatasreceives, and code afteryieldis the teardown.- Multiple managers in one
withare nested.with a, b:is equivalent towith a: with b:.
Warning:
Suppression is powerful and easy to misuse. Returning
Truefrom__exit__makes the exception vanish. Doing this by accident — for example, returningself— silently swallows every error in the block. ReturnFalseorNoneunless you are deliberately handling the exception.
The syntax you will use
Class-based context manager. Implement both methods. __exit__ receives the exception details, or three Nones.
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
import time
self.elapsed = time.perf_counter() - self.start
return False # never suppress
contextlib.contextmanager for the simple case. Write a generator with exactly one yield; everything before it is setup and everything after is teardown. Use try/finally around the yield so cleanup runs on error.
from contextlib import contextmanager
@contextmanager
def timed(label):
import time
start = time.perf_counter()
try:
yield # the with-block executes here
finally:
print(f"{label}: {time.perf_counter() - start:.3f}s")
with timed("load"):
do_work()
Binding the yielded value. Whatever you yield becomes the value after as.
@contextmanager
def connection(url):
conn = connect(url)
try:
yield conn # `as conn` receives this
finally:
conn.close()
with connection("db://...") as conn:
conn.execute("SELECT 1")
Multiple managers in one statement. They are entered left to right and exited right to left.
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
contextlib.suppress for expected errors. It is a readable alternative to an empty except block.
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("maybe.txt")
contextlib.closing for objects that have close() but are not context managers.
from contextlib import closing
with closing(open("data.txt")) as f:
data = f.read()
contextlib.ExitStack for a variable number of resources. This is the tool when you do not know up front how many things to clean up.
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
# all files close together when the block ends
Async context managers. For async code, use async with and implement __aenter__ / __aexit__.
async with session.get(url) as response:
data = await response.json()
Examples: simple to real
Example 1 — the resource guarantee.
with open("data.csv") as f:
rows = f.readlines()
process(rows) # even if this raises, f is closed
Compare this with the manual version at the top of the page. There is no close() to forget, and the cleanup is attached to the resource rather than buried in the body.
Example 2 — a database transaction.
@contextmanager
def transaction(conn):
conn.begin()
try:
yield conn
conn.commit() # only on success
except Exception:
conn.rollback() # on any failure
raise # do not swallow
finally:
conn.close()
with transaction(pool.connect()) as conn:
conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
conn.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
Either both updates apply or neither does. If the second statement fails, the rollback undoes the first, and the raise makes the failure visible to the caller. This is the shape of every real transaction helper.
Example 3 — a lock that is always released.
from threading import Lock
lock = Lock()
with lock: # acquires on enter, releases on exit
update_shared_state()
If update_shared_state() raises, the lock is still released. A manual acquire()/release() pair that misses the release deadlocks every other thread.
Example 4 — timing, with correct cleanup.
@contextmanager
def timed(label):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
log.info("%s took %.3fs", label, elapsed)
with timed("retrieve"):
results = retriever.search(query)
Using try/finally around the yield means the timing is recorded even when retriever.search raises — which is exactly when you most want the number.
Example 5 — suppressing a predictable failure.
from contextlib import suppress
with suppress(KeyError, RedisError):
cache.delete(key) # a cache miss or outage must not break the request
Be careful: this hides the error completely. Suppression belongs only where the failure is genuinely acceptable and understood.
In production
- Prefer
withover manual acquire/release. Every unguarded resource is a leak waiting for an exception. This is the cheapest correctness win in Python. - Return
Falsefrom__exit__unless suppressing is the point. Returning a truthy value by accident swallows errors.contextlib.suppressexists precisely so that suppression is explicit and named. - Use
try/finallyinside@contextmanagergenerators. An exception in the block is thrown into the generator at theyieldpoint. Withouttry/finally, your cleanup code afteryieldmay be skipped. - Never swallow exceptions silently. Log with the traceback, or convert to a domain error and re-raise. A bare
except: passinside a context manager is how outages become invisible. - Keep
__exit__from raising. If cleanup itself fails, it can replace the original exception, making the real cause much harder to find. Log cleanup failures; do not mask the original. - Use
ExitStackfor dynamic cleanup. Spawning a variable number of tasks, opening several optional files, or registering callbacks is exactly its job. It also makes cleanup order explicit (last in, first out). - Remember the guarantee includes
return. Areturninside awithstill runs__exit__, so a partially built result can be cleaned up correctly. Do not disable cleanup with flags out of worry. - Watch for long-lived context managers. Holding a connection or lock for the whole request is right; holding it across the entire process is a bottleneck. Acquire late and release early.
- Use
async within async code. A regularwitharound an async resource either does not work or blocks the event loop. Match the context manager to the execution model.
Interview questions
1. What problem do context managers solve?
Answer. Guaranteed cleanup. They attach setup and teardown to a block so the teardown runs even if the block raises, returns, or breaks. This prevents leaked files, connections, locks, and transactions — resource leaks that are usually invisible until the service fails under load.
Follow-up: “How did people do this before with?” With try/finally. That is still the underlying mechanism; with just packages it with the resource so it cannot be forgotten.
Trap. Saying context managers are only for files. They are for any acquire/release pair, including locks, transactions, temporary state, and timing.
2. What are __enter__ and __exit__ responsible for?
Answer. __enter__ runs at the start of the block and its return value is bound by as. __exit__ runs at the end and receives the exception type, value, and traceback, or three Nones on normal completion. Returning a truthy value from __exit__ suppresses the exception.
Follow-up: “What does __exit__ receive when there is no error?” (None, None, None). Checking exc_type is None is how a manager detects normal completion.
Trap. Returning self from __exit__. That is truthy, so it silently suppresses every exception in the block.
3. How does @contextlib.contextmanager work?
Answer. It turns a generator function into a context manager. Code before yield is the setup, the yielded value is what as receives, and code after yield is the teardown. If the block raises, the exception is thrown into the generator at the yield point.
Follow-up: “Why wrap the yield in try/finally?” So cleanup runs even when the block raises. Without it, code after yield can be skipped.
Trap. Writing more than one yield, or none. The decorator requires exactly one yield; it raises a RuntimeError otherwise.
4. What does it mean for __exit__ to suppress an exception?
Answer. If __exit__ returns a truthy value, the exception is considered handled and does not propagate; execution continues after the with block. Returning False or None lets it propagate normally.
Follow-up: “When is suppression appropriate?” Only when the failure is expected and safe to ignore, such as deleting a missing cache key. Use contextlib.suppress to make that intent explicit.
Trap. Using suppression as a substitute for error handling. It hides real failures and makes debugging much harder.
5. Does __exit__ run if the block returns early or raises?
Answer. Yes. The with statement guarantees __exit__ on every path: normal completion, exception, return, break, and continue. That is exactly why the statement is more reliable than a manual cleanup call.
Follow-up: “What if __exit__ itself raises?” Its exception replaces the original one, unless it handles the situation first. That is why cleanup code should not raise carelessly.
Trap. Assuming return skips cleanup. It does not.
6. When would you use ExitStack?
Answer. When the number of resources is not known until runtime: opening a variable list of files, entering a variable set of context managers, or registering cleanups as you go. ExitStack tracks them and unwinds them in reverse order, so the cleanup order is well defined.
Follow-up: “How does it relate to nesting with statements?” It is the dynamic equivalent. with a, b, c: is nesting for a fixed count; ExitStack handles a count you only know at runtime.
Trap. Manually tracking a list of opened resources and forgetting one on an early error path. ExitStack exists to remove that class of bug.
7. What is the difference between with and async with?
Answer. with uses __enter__ and __exit__, which are synchronous. async with uses __aenter__ and __aexit__, which are awaited, so they can perform I/O — for example, opening a network connection — without blocking the event loop.
Follow-up: “Can you use with on an async resource?” Not correctly. Either the protocol is missing, or the synchronous variant blocks the event loop, which harms all concurrent tasks.
Trap. Mixing the two, such as using a synchronous database driver inside async code. That blocks the loop and looks like a performance mystery.
8. How do you write a context manager that is both correct and safe to reuse?
Answer. Keep __enter__ and __exit__ idempotent and stateless where possible, return False from __exit__ unless suppression is intentional, and use try/finally in generator-based managers. If the manager stores per-use state, do not reuse one instance across concurrent uses.
Follow-up: “What is a sign a context manager is unsafe?” It stores mutable state on self during __enter__ and relies on it during __exit__, so overlapping uses corrupt each other. Prefer creating a fresh manager per block.
Trap. Reusing a single manager instance concurrently in threaded code and assuming each with is isolated.
Remember this
- A context manager guarantees setup before and cleanup after a block, on every exit path.
- Implement
__enter__/__exit__, or write a generator with oneyieldand@contextmanager. __exit__receives the exception; returnFalse/Noneunless you mean to suppress.- Wrap the
yieldintry/finally, or cleanup can be skipped. - Use
ExitStackwhen the number of resources is only known at runtime.