Context Managers
with guarantees that some cleanup action runs when the block exits — normally, via return, or via an exception propagating out — without needing an explicit try/finally. open() returns a context manager: an object that knows how to set something up on entry and tear it down on exit.
🎯 What you'll learn
- The
withStatement — Guaranteed Cleanup - The Protocol:
__enter__and__exit__ - Suppressing Exceptions via
__exit__'s Return Value contextlib.contextmanager— Generator-Based Context Managerscontextlib.suppress— a Built-In for a Common Pattern
1The with Statement — Guaranteed Cleanup
with open("data.txt") as f:
contents = f.read()
# f is automatically closed here, even if .read() raised an exceptionwith guarantees that some cleanup action runs when the block exits — normally, via return, or via an exception propagating out — without needing an explicit try/finally. open() returns a context manager: an object that knows how to set something up on entry and tear it down on exit.
2The Protocol: __enter__ and __exit__
class Resource:
def __enter__(self):
print("acquiring")
return self # becomes the `as` target
def __exit__(self, exc_type, exc_value, traceback):
print("releasing")
return False # False (or None) = do NOT suppress any exception
with Resource() as r:
print("using", r)Output:
acquiring
using <Resource object ...>
releasingwith EXPR as name: desugars, roughly, to calling EXPR.__enter__() and binding its return value to name, running the block, then always calling EXPR.__exit__(exc_type, exc_value, traceback) on the way out — whether the block finished normally (all three arguments None) or an exception is propagating through it (all three populated).
3Suppressing Exceptions via __exit__'s Return Value
class SwallowValueError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is ValueError:
print(f"suppressed: {exc_value}")
return True # True = suppress this exception, block exits normally
return False # anything else propagates as usual
with SwallowValueError():
raise ValueError("boom") # caught and suppressed by __exit__
print("still running") # this DOES run — the exception never escaped the with blockIf __exit__ returns a truthy value, the exception that was propagating through the with block is suppressed entirely — code after the with block continues as if nothing happened. Returning False/None (the default if you don't explicitly return anything) lets the exception propagate normally after cleanup runs. This is deliberate, explicit opt-in behavior — accidentally suppressing exceptions by forgetting to return False is a real footgun (see Common Pitfalls).
4contextlib.contextmanager — Generator-Based Context Managers
from contextlib import contextmanager
@contextmanager
def resource():
print("acquiring")
try:
yield "the resource" # this becomes the `as` target
finally:
print("releasing") # ALWAYS runs, exception or not
with resource() as r:
print("using", r)Writing a full class with __enter__/__exit__ is verbose for simple cases. @contextmanager turns a generator function into a context manager: everything before yield is the "enter" logic, the yielded value is the as target, and everything after yield (typically in a finally block) is the "exit" logic. An exception raised inside the with block is re-raised at the yield point inside the generator, which is exactly why the cleanup code needs to sit in a finally (or a try/except around the yield) to run reliably.
5contextlib.suppress — a Built-In for a Common Pattern
from contextlib import suppress
with suppress(FileNotFoundError):
import os
os.remove("maybe_missing.txt")
# no exception, no error, whether the file existed or notsuppress(*exception_types) is a ready-made context manager equivalent to a try/except: pass for exactly the listed exception types — useful for "best-effort cleanup" code where a specific, expected failure should be silently ignored, without writing a full try/except for a one-liner.
6Multiple Context Managers in One with
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())Comma-separating context managers in one with statement enters them left to right and exits them in reverse order (right to left) — equivalent to nesting them, but flatter and more readable. If entering the second one raises, the first one's __exit__ still runs correctly (Python handles this internally, equivalent to nested with blocks under the hood).
Under the HoodWhat with Actually Desugars To▾
with EXPR as name:
BODYis equivalent to:
mgr = EXPR
aexit = type(mgr).__exit__ # looked up on the TYPE, not the instance
aenter = type(mgr).__enter__
value = aenter(mgr)
exc = True
try:
try:
name = value
BODY
except:
exc = False
if not aexit(mgr, *sys.exc_info()):
raise
finally:
if exc:
aexit(mgr, None, None, None)The key detail most people miss: __enter__/__exit__ are looked up on the object's type, not the instance — the same special-method lookup rule that applies to operator overloading (__add__, __len__, etc.) applies here too, meaning you can't monkey-patch __exit__ onto a single instance and have with see it. This is also exactly why @contextmanager-decorated generator functions work: the decorator returns a class (_GeneratorContextManager) whose __enter__/__exit__ methods drive the generator via next()/.throw(), translating the class-based protocol into the generator's yield-based one underneath.
⚠️ Common Pitfalls
- Forgetting to
return False/Nonefrom a custom__exit__— if the last statement happens to evaluate truthy (e.g.return exc_type is Nonewhen there was no exception evaluates toTrue), it silently suppresses real exceptions raised inside the block; be explicit about the return value. - Writing
@contextmanagercleanup logic withouttry/finallyaround theyield— if the block raises, that exception is re-raised at theyieldpoint; withoutfinally, an exception skips the cleanup code entirely, defeating the purpose of using a context manager at all. - Reusing a single context manager instance across multiple
withblocks concurrently — most context managers (including file objects) are not designed to be entered twice at once; eachwithshould generally get its own instance unless the class documents reentrancy support. - Assuming
contextlib.suppresscatches subclasses you didn't list — it uses normal exception matching, sosuppress(ValueError)does not catch aTypeError, even in code where you expected "any input-related error." - Using a bare
try/finallyinstead ofwithfor resources that already provide a context manager — reinvents cleanup logic that's already correctly handled (including the exception-info threading through__exit__), and is easy to get subtly wrong (e.g. forgetting to close on every exception path).
📋 Quick Reference
with EXPR as name: # __enter__() -> name, guarantees __exit__() on exit
...
class C:
def __enter__(self): return self # setup, returns the `as` target
def __exit__(self, exc_type, exc_val, tb): # ALWAYS called; return True to suppress exception
return False
from contextlib import contextmanager
@contextmanager
def cm():
setup()
try:
yield value # the `as` target
finally:
teardown() # always runs
from contextlib import suppress
with suppress(SomeError): ... # try/except SomeError: pass, as a context manager
with a() as x, b() as y: ... # enters left-to-right, exits right-to-left🎤 Interview Q&A
Q1What is a context manager?core▾
An object used with with that sets up and tears down a resource reliably: __enter__ runs on entry, __exit__ on exit even if an exception occurs. Used for files, locks, transactions, connections.
- Can __exit__ suppress exceptions? Yes, by returning True.
Q2How do you write one with contextlib?core▾
@contextmanager def timer(): start = perf_counter(); try: yield; finally: log(perf_counter() - start). Code before yield is setup, after is teardown.
- Why try/finally around yield? Ensures cleanup on exceptions.
Q3What does ExitStack do?core▾
Manages a dynamic number of context managers, entering them in a loop and exiting all in reverse order, even on errors.
- Use case? Opening a variable list of files.
Q4What are async context managers?senior▾
Objects with __aenter__/__aexit__ used with async with, for async resources like DB sessions or HTTP clients; @asynccontextmanager creates them.
- Example?
async with httpx.AsyncClient() as client:.
Q5Give practical examples in backend code.core▾
Database transactions (with session.begin():), locks (with lock:), temporary files/directories, patching in tests (with mock.patch(...)), and suppressing specific errors (contextlib.suppress).
- Why transactions as context managers? Automatic commit/rollback.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.