Context Managers
The problem context managers solve
Section titled “The problem context managers solve”Many operations must be paired with a cleanup step: open a file then close it, acquire a lock then release it, start a timer then stop it. If an exception occurs between the two steps, the cleanup never runs — leading to resource leaks.
The with statement solves this by guaranteeing that the cleanup runs no matter what.
with open("data.txt") as f: content = f.read()# f is always closed here, even if read() raisedHow with works: the protocol
Section titled “How with works: the protocol”Any object with __enter__ and __exit__ methods is a context manager.
with expr as var: bodyThis is exactly equivalent to:
import sys
cm = exprvar = cm.__enter__()try: bodyexcept: if not cm.__exit__(*sys.exc_info()): raiseelse: cm.__exit__(None, None, None)__enter__ sets up the resource and returns a value bound to var.
__exit__ always runs on exit. If it returns a truthy value it suppresses the exception; returning False (or None) lets the exception propagate.
The bare except: above is one of the rare cases where catching everything is correct: the if not ...: raise re-raises unless __exit__ explicitly suppresses, so no exception is ever silently swallowed.
flowchart TD A["cm.__enter__()"] --> B["run the with body"] B -->|"no exception"| C["cm.__exit__(None, None, None)"] B -->|"exception raised"| D["cm.__exit__(exc_type, exc, tb)"] D -->|"returns truthy"| E["exception suppressed -- execution continues after with"] D -->|"returns falsy"| F["exception re-raised -- propagates past the with"]
Writing a class-based context manager
Section titled “Writing a class-based context manager”from types import TracebackTypefrom typing import Self
class Timer: def __enter__(self) -> Self: print("Timer started") return self
def __exit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, ) -> bool: print("Timer stopped") suppress = False # do not suppress exceptions return suppressThe three arguments to __exit__ are the exception type, value, and traceback — all None when no exception occurred.
__exit__ returning True swallows the exception
Section titled “__exit__ returning True swallows the exception”The single most consequential line in any __exit__ implementation is its return value. Returning anything
truthy — including a bare return True typo’d in place of return False — tells the with statement the
exception has been handled, and it simply vanishes:
class Swallower: def __enter__(self) -> Self: return self def __exit__(self, exc_type, exc, tb) -> bool: if exc is not None: print(f"swallowing {type(exc).__name__}: {exc}") return True # BUG-PRONE if unintentional: any exception disappears here
with Swallower(): raise ValueError("this never propagates")print("execution continues here -- the ValueError above is gone")This is occasionally exactly what you want (contextlib.suppress, below, is built entirely on this
behavior) — but an __exit__ that returns True unconditionally, written by accident rather than on
purpose, silently deletes every exception raised inside every with block that uses it. Always return
False (or None, or nothing) unless you specifically mean to suppress.
contextlib.contextmanager — the generator shortcut
Section titled “contextlib.contextmanager — the generator shortcut”Writing a full class just to wrap setup/teardown is verbose.
contextlib.contextmanager lets you use a generator function instead:
import contextlib
@contextlib.contextmanagerdef managed_resource(name: str): print(f"Acquiring {name}") try: yield name # value bound to the as-variable finally: print(f"Releasing {name}")Everything before yield is __enter__ logic. Everything after yield (in the finally) is __exit__ logic.
The try/finally inside the generator ensures the teardown runs even when an exception is raised inside the with block.
contextlib.suppress — __exit__ returning True, packaged
Section titled “contextlib.suppress — __exit__ returning True, packaged”contextlib.suppress(*exception_types) is a context manager whose __exit__ returns True — but only when
the exception raised matches one of the types you named:
import contextlib
with contextlib.suppress(FileNotFoundError): open("does-not-exist.txt")# execution continues here either wayIt is the disciplined version of the Swallower example above: instead of an __exit__ that swallows
everything, it swallows exactly the exception types you listed, and nothing else propagates differently
than it otherwise would. See the Exceptions lesson for how this compares to try/except/pass.
ExitStack — a dynamic number of context managers
Section titled “ExitStack — a dynamic number of context managers”Nesting a with statement works when you know the resources up front. contextlib.ExitStack handles a
number of context managers decided at runtime — opening a variable-length list of files, for example — and
guarantees they all unwind in reverse order, even if one fails partway through:
import contextlib
names = ["a", "b", "c"]with contextlib.ExitStack() as stack: resources = [stack.enter_context(managed_resource(n)) for n in names] stack.callback(print, "extra cleanup callback ran") # use resources here# everything registered with enter_context/callback unwinds here, most-recently-registered firststack.enter_context(cm) calls cm.__enter__() immediately and registers cm.__exit__ to run when the
ExitStack itself exits. stack.callback(fn, *args) registers a plain function to run at the same point,
for cleanup that is not itself a context manager. Everything registered unwinds in LIFO order — last
registered, first to run — exactly like nested with statements unwind from the inside out.
Nesting context managers
Section titled “Nesting context managers”Multiple context managers can be combined on one with line — they nest left to right:
with open("in.txt") as src, open("out.txt", "w") as dst: dst.write(src.read())async with — a teaser
Section titled “async with — a teaser”An asynchronous context manager implements __aenter__ and __aexit__ as coroutine methods instead of
plain methods, and is used with async with inside an async def function:
class AsyncTimer: async def __aenter__(self) -> Self: print("async timer started") return self async def __aexit__(self, exc_type, exc, tb) -> bool: print("async timer stopped") return False
async def main() -> None: async with AsyncTimer(): print("doing async work...")The protocol is otherwise identical to the synchronous version — same three arguments, same meaning for the
return value. The Async & Concurrency module covers async/await, tasks, and the event loop in depth;
this is only enough to recognize the shape when you see it.
Full runnable demo
Section titled “Full runnable demo”import contextlibfrom collections.abc import Iteratorfrom types import TracebackTypefrom typing import Self
# --- class-based context manager ---class Timer: def __enter__(self) -> Self: print("Timer started") return self def __exit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, ) -> bool: print("Timer stopped") suppress = False return suppress
print("=== class-based ===")with Timer(): print(" doing work...")
# --- contextlib.contextmanager ---@contextlib.contextmanagerdef managed_resource(name: str) -> Iterator[str]: print(f"Acquiring {name}") try: yield name finally: print(f"Releasing {name}")
print("\n=== contextmanager generator ===")with managed_resource("connection") as res: print(f" using {res}")
# --- cleanup on error ---print("\n=== cleanup runs even on error ===")try: with managed_resource("db") as res: print(f" using {res}") raise RuntimeError("something failed")except RuntimeError as e: print(f" caught: {e}")Loading Python runtime (first run only)…
Depth demo: everything above, running
Section titled “Depth demo: everything above, running”import asyncioimport contextlibfrom collections.abc import Iteratorfrom types import TracebackTypefrom typing import Self
# --- __exit__ returning True swallows the exception ---print("=== __exit__ returning True swallows ===")class Swallower: def __enter__(self) -> Self: return self def __exit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, ) -> bool: if exc is not None: print(f" swallowing {type(exc).__name__}: {exc}") return True # BUG-PRONE if unintentional: any exception disappears here
with Swallower(): raise ValueError("this never propagates")print(" execution continues here -- the ValueError above is gone")
# --- contextlib.suppress is built on exactly this pattern ---print("=== contextlib.suppress ===")with contextlib.suppress(FileNotFoundError): open("does-not-exist.txt")print(" suppress(FileNotFoundError).__exit__ returns True only for FileNotFoundError")
# --- ExitStack: a dynamic number of context managers ---print("=== ExitStack ===")@contextlib.contextmanagerdef resource(name: str) -> Iterator[str]: print(f" acquiring {name}") try: yield name finally: print(f" releasing {name}")
names = ["a", "b", "c"]with contextlib.ExitStack() as stack: resources = [stack.enter_context(resource(n)) for n in names] print(f" using {resources}") stack.callback(print, " extra cleanup callback ran")# unwinds LIFO: the callback (registered last) runs first, then c, then b, then a
# --- async with teaser ---print("=== async with (teaser) ===")class AsyncTimer: async def __aenter__(self) -> Self: print(" async timer started") return self async def __aexit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, ) -> bool: print(" async timer stopped") return False
async def main() -> None: async with AsyncTimer(): print(" doing async work...")
asyncio.run(main())Loading Python runtime (first run only)…
Mental Model
Section titled “Mental Model”A context manager is a promise with two halves: __enter__ always runs before the body, and — no matter
how the body exits, normally, via return, or via any exception — __exit__ always runs after it. The
with statement is exactly the try/except/finally shape shown near the top of this lesson, spelled
differently; __exit__’s return value is the one place that shape deviates from a plain finally block,
since it can actively choose to suppress what would otherwise propagate.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Class-based context manager | Full control, can hold state across __enter__/__exit__ | More boilerplate than a generator |
contextlib.contextmanager | One function, try/finally reads naturally | Only a single yield — cannot be re-entered like some class-based managers can |
contextlib.suppress | One line, states exactly what is ignored | Only for the narrow case of “ignore these specific exception types” |
ExitStack | Handles a variable number of resources safely | Slightly more indirect than a literal with a, b, c: |
Common Misconceptions
Section titled “Common Misconceptions”- “
__exit__returningFalsemeans something went wrong.”False(orNone, or nothing) is the normal, correct default — it means “let the exception propagate as it would without this context manager.” ReturningTrueis the unusual choice that needs a reason. - “A
withstatement only matters for exceptions.” It also guarantees cleanup runs on a normalreturnfrom inside the block, or abreak/continueout of an enclosing loop — any way the body can exit. - “
contextlib.contextmanagerfunctions can be reused across multiplewithstatements.” A generator is exhausted after one full run through; call the decorated function again for a newwithblock rather than reusing the same generator object. - “
ExitStackis only useful for files.” It works for any context manager (locks, network connections, temporary state), and for plain cleanup callbacks that are not context managers at all.
Reading AI-generated code
Section titled “Reading AI-generated code”Before trusting a generated context manager, check for these:
- Does
__exit__returnTrue(or something obviously truthy) without a clear, commented reason? That silently deletes every exception raised inside everywithblock using it. - Is a resource opened and closed manually (
f = open(...)/f.close()) instead ofwith open(...) as f:? Any exception between the two calls skips theclose(). - Does a
@contextlib.contextmanagergenerator do its cleanup outside atry/finally? If the code betweenyieldand cleanup can raise, or if thewithbody itself raises, cleanup written withoutfinallynever runs. - Is a fixed, hardcoded number of
withstatements used where the number of resources is actually dynamic (a list of files, a variable number of connections)? That usually meansExitStackwas needed. - Is
contextlib.suppress(or a bareexcept: pass) hiding an exception that should have been logged? Suppressing is a decision, not a default.
🐛 Spot the bug
class DatabaseTransaction: def __enter__(self): print("BEGIN") return self
def __exit__(self, exc_type, exc, tb): if exc_type is not None: print("ROLLBACK") else: print("COMMIT") return True
with DatabaseTransaction(): print("INSERT INTO users ...") raise ValueError("insert failed")
print("continuing after the transaction")Show the answer
__exit__ returns True unconditionally — even on the ROLLBACK path where an exception actually occurred. That means ValueError("insert failed") is silently swallowed: the code prints BEGIN, INSERT INTO users ..., ROLLBACK, then continuing after the transaction, with no trace that anything failed. The caller has no way to know the insert did not succeed. The fix is return False (or no return at all) so the exception still propagates after rollback — __exit__ should decide whether to run cleanup, not whether to hide the failure.