Skip to content

Context Managers

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() raised

Any object with __enter__ and __exit__ methods is a context manager.

with expr as var:
body

This is exactly equivalent to:

import sys
cm = expr
var = cm.__enter__()
try:
body
except:
if not cm.__exit__(*sys.exc_info()):
raise
else:
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"]
The with-statement protocol: __exit__'s return value decides whether an exception escapes
from types import TracebackType
from 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 suppress

The 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.contextmanager
def 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 way

It 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 first

stack.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.

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())

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.

import contextlib
from collections.abc import Iterator
from types import TracebackType
from 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.contextmanager
def 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}")
import asyncio
import contextlib
from collections.abc import Iterator
from types import TracebackType
from 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.contextmanager
def 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())

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.

What you getBenefitCost
Class-based context managerFull control, can hold state across __enter__/__exit__More boilerplate than a generator
contextlib.contextmanagerOne function, try/finally reads naturallyOnly a single yield — cannot be re-entered like some class-based managers can
contextlib.suppressOne line, states exactly what is ignoredOnly for the narrow case of “ignore these specific exception types”
ExitStackHandles a variable number of resources safelySlightly more indirect than a literal with a, b, c:
  • “__exit__ returning False means something went wrong.” False (or None, or nothing) is the normal, correct default — it means “let the exception propagate as it would without this context manager.” Returning True is the unusual choice that needs a reason.
  • “A with statement only matters for exceptions.” It also guarantees cleanup runs on a normal return from inside the block, or a break/continue out of an enclosing loop — any way the body can exit.
  • “contextlib.contextmanager functions can be reused across multiple with statements.” A generator is exhausted after one full run through; call the decorated function again for a new with block rather than reusing the same generator object.
  • “ExitStack is 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.

Before trusting a generated context manager, check for these:

  • Does __exit__ return True (or something obviously truthy) without a clear, commented reason? That silently deletes every exception raised inside every with block using it.
  • Is a resource opened and closed manually (f = open(...) / f.close()) instead of with open(...) as f:? Any exception between the two calls skips the close().
  • Does a @contextlib.contextmanager generator do its cleanup outside a try/finally? If the code between yield and cleanup can raise, or if the with body itself raises, cleanup written without finally never runs.
  • Is a fixed, hardcoded number of with statements used where the number of resources is actually dynamic (a list of files, a variable number of connections)? That usually means ExitStack was needed.
  • Is contextlib.suppress (or a bare except: 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.

What two methods must an object implement to be used as a context manager?
In a @contextlib.contextmanager generator, what marks the boundary between setup and teardown?
When does the __exit__ method run?
What does this print?
class CM:
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc, tb):
        return True

with CM():
    raise ValueError("x")
print("after")
What does stack.enter_context(cm) do inside a with contextlib.ExitStack() as stack: block?
When the with block exits, in what order do the callback and the resource's cleanup run?
with contextlib.ExitStack() as stack:
    stack.callback(print, "first")
    stack.enter_context(managed_resource("r"))
Why is async def __aenter__ / async def __aexit__ used instead of __enter__ / __exit__?