Python Context Managers: with, contextlib, and Async
A Python context manager is the with protocol: __enter__/__exit__, contextlib helpers, custom classes, and async with. Files are an example, not the definition.

A Python context manager is the with protocol: __enter__/__exit__, contextlib helpers, custom classes, and async with. Files are an example, not the definition.

A Python context manager is an object that implements `enter and exit and controls the environment inside a with` block. PEP 343 added with in Python 2.5, released 19 Sep 2006, so you can factor out standard try/finally uses. Files, threading.Lock, and decimal.localcontext() are examples of the protocol, not the definition.
You have already used one if you have written with open(...) as f. Daniel Porteous put it this way at PyCon AU: lots of people do not know context managers by name, and pretty much everyone has used them. It is not an if-statement, and not contextvars.
/ exit`), not "the thing that closes files."with always calls `exit after a successful enter, including when the as` target assignment fails. swallows the exception. That is the difference from a plain finally`.@contextmanager instances and files are single-use. Create them in the with header.async with is a parallel protocol (`aenter / aexit`), added with PEP 492 in Python 3.5.The glossary definition: an object which implements the context management protocol and controls the environment seen in a with statement. See PEP 343.
The data model lists typical uses: saving and restoring global state, locking and unlocking resources, closing opened files. Files are one row on that list.
On r/learnpython, beginners often look for a "default" manager, as if Python shipped one special object. u/Temporary_Pie2733 (July 2025) shut that down: there is no default context manager. Any type can implement the protocol, and files, locks, and decimal contexts already do.
*"A context manager is simply 'an object that can be used in a with statement'. Technically, this means the object's class defines enter and exit methods, which the with statement calls automatically."*
u/lfdfq in r/learnpython (July 2025)
CPython, the Python Software Foundation reference implementation, runs with by calling those two methods. Do not rely on `del` to close files or release locks.
PEP 343 (Guido van Rossum and Alyssa Coghlan, created 13 May 2005) exists because try/finally cleanup was copied in every caller. The statement is sugar for a reusable enter/exit pair.
Python 2.5 needed a future import (`from future import with_statement). From 2.6 onward, with` is always on.
The language reference runs with in a fixed order. Methods are loaded before `enter` runs (implicit special-method lookup).
and exit` from the result.. If that raises, exit` is not called.as target if you wrote one., with three None`s on a clean exit.If `enter() returns without error, exit() always runs, including when the as` assignment fails. Wrap cleanup only for resources you actually acquired.
as is optional. `enter may return self, a resource, or None. with open(...) as f returns the file object, which is why f` is the file, not the opener.
Beginner tutorials flatten with into try/finally. Close, but not the same: `exit` can inspect the exception and silence it.
If `exit returns a true value, the exception is suppressed. False or None` lets it propagate.
`exit` should not reraise the exception it was passed. That is the caller's job.
A cleanup helper that returns a truthy status, then return cleanup(), swallows bugs. Default `exit to return False`. Stack Overflow 26096435 is the write-up of that gap.
James Murphy (mCoding) still prefers with when the object already knows its cleanup, because try/finally puts file.close() on every caller. Prefer with when the protocol exists. Prefer try/finally for a one-off inside a single function.
Multiple with items nest: first entered, last exited. The comma form has been in the language since 3.1.
with open("a.txt") as src, open("b.txt", "w") as dst:
dst.write(src.read())Python 3.10 added parenthesized multi-line with, including a trailing comma. Use that when the header would wrap.
On r/learnpython, people hit this through SQLAlchemy: with engine.connect() as conn, conn.begin() looks like connect() returned a tuple. It did not. It is two context managers, stacked.
If open itself is still new, start with Python basics.
Official contextlib docs split instances by reuse. That split is what explains the RuntimeError: generator didn't yield you hit on a second with.
Type | Best For | Key Characteristics |
|---|---|---|
Single-use | Files after close; | Reuse raises. Create the instance in the |
Reusable, not reentrant | Locks that deadlock on re-acquire | Successive |
Reentrant |
| Nested |
PEP 343 already flagged files and generator-based managers as single-use. Write with open(path) as f, not cm = open(path) followed by with cm later.
A second axis is how you implement one: a class with dunders, a generator wrapped in @contextmanager, or the async pair `aenter / aexit`.
Promote to a context manager when other callers must not forget the cleanup.
Do not call `enter and exit by hand, except through ExitStack.enter_context`.
The protocol is two methods. Return False from `exit` unless you mean to swallow.
import os
class env_var:
def __init__(self, key, value):
self.key = key
self.value = value
self._prior = None
def __enter__(self):
self._prior = os.environ.get(self.key)
os.environ[self.key] = self.value
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self._prior is None:
os.environ.pop(self.key, None)
else:
os.environ[self.key] = self._prior
return FalseUse a class when you need instance state, extra methods, or reuse across successive with blocks. `enter can return self or a resource the caller binds with as`.
If `enter raises, exit does not run. Acquire first, then return. Put release in exit` only.
@contextmanager shipped with contextlib in 2.5. It is a decorator that yields once: one yield, then teardown.
from contextlib import contextmanager
import time
@contextmanager
def timed(label):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")Yield exactly once. Zero yields becomes RuntimeError: generator didn't yield. A second yield becomes RuntimeError: generator didn't stop after throw().
Wrap yield in try/finally, or teardown is skipped when the body raises. The exception is thrown into the generator.
Official docs put try/finally in the canonical example. CPython #77649 shows what happens when you forget.
Instances are single-use. Reusing the same generator-wrapper object raises RuntimeError: generator didn't yield. Create them in the with header: with timed("load"):, not cm = timed("load") parked on a variable.
If the generator logs an exception and does not want to suppress it, it must reraise. The True/False `exit` protocol does not apply inside the generator.
Two camps exist. Do not collapse them.
@contextmanager, because a hand-written `exit` is easy to get wrong.try/finally around yield, "which to me kind of defeats the purpose."Use the class when you need state, reuse, or extra methods. Use @contextmanager when enter and exit are two linear chunks. Neither is required.
@contextmanager is a decorator that yields once. ContextDecorator (3.2) makes a context manager usable as a decorator by building a new generator per call. Decorator stacking belongs in a decorator article, not here.
Python 3.14's contextlib module lists more helpers than you need. Five to seven cover the jobs people actually hit.
Helper | Added | Use it when |
|---|---|---|
| 2.5 | Enter/exit are two linear chunks. |
| 3.2 | You want the same helper as |
| 3.3 | N resources, optional resources, or input-driven enter. |
| 3.4 | You intend to silence one specific error. |
| A branch might skip opening anything. Faster no-op than | |
| 2.5 era | The object has |
| 3.4 | Capture prints. Reentrant. Not for subprocesses. |
ExitStack is LIFO: enter_context, callback, push, pop_all. Official docs call it out for resources that are optional or driven by input data.
Alyssa Coghlan prototyped it in contextlib2 before it landed in 3.3. You do not need to install contextlib2 on 3.14.
from contextlib import ExitStack
def concat(paths, dest):
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
out = stack.enter_context(open(dest, "w"))
for f in files:
out.write(f.read())nullcontext is the one-resource answer. On Reddit, people prefer nullcontext(sys.stdin) over a one-item ExitStack when a flag might skip opening a file. Closing stdin for the rest of the process is a live footgun.
from contextlib import nullcontext
import sys
def read(path=None):
ctx = open(path) if path else nullcontext(sys.stdin)
with ctx as fh:
return fh.read()suppress is the intentional silencer. The docs warn: very specific errors.
from contextlib import suppress
import os
path = "scratch.tmp"
with suppress(FileNotFoundError):
os.remove(path)Empty suppress() is Ruff B022. Do not use `exit returning true as a casual except: pass`.
closing(thing) calls thing.close(). Use it for urllib-style objects that never grew with support. Do not confuse it with "how do I close a file." with open already does that.
Non-file examples worth typing: threading.Lock(), decimal.localcontext(), contextlib.suppress(FileNotFoundError), unittest.mock.patch, a DB connect / begin pair. with open remains the teaching example.
An asynchronous context manager can suspend in enter and exit. PEP 492 (Yury Selivanov) shipped that in Python 3.5, released 13 Sep 2015.
`aenter and aexit must return awaitables. The statement is async with EXPR as VAR`.
That is a SyntaxError outside async def. Passing a regular sync context manager to async with is an error.
from contextlib import asynccontextmanager
@asynccontextmanager
async def connected(pool):
conn = await pool.acquire()
try:
yield conn
finally:
await pool.release(conn)@asynccontextmanager landed in 3.7, same release as AsyncExitStack and nullcontext. AsyncExitStack combines sync and async managers.
Close it with aclose(), not close(). Do not install the 2018 async-exit-stack backport on a current Python.
Cancellation still runs `aexit`. You cannot skip it by "forcing" exit.
nullcontext gained async support in 3.10. The helper itself is 3.7.
On Reddit, mixed sync/async nesting is the "staircase of doom." AsyncExitStack is the current workaround.
PEP 806, a proposal for mixed items in one with header, was rejected. Do not write it as if it ran today.
with puts cleanup on the object, so callers stop forgetting close() on return, break, continue, and exception.
That guarantee still fails on os._exit, an OS kill, or a KeyboardInterrupt in the enter/exit race. Murphy shows built-in C locks survive that race.
A pure-Python wrapper may not. That is a language limit, not a library bug.
Murphy wrapped Dear ImGui begin/end so a forgotten end became impossible. Same shape as wrapping a third-party API that has begin/end and no dunders: `enter calls begin and may return that result, not self`.
A plain finally cannot see why the block ended unless you restructure the except. `exit receives the triple. Returning true is how suppress` works. Used on purpose, that is a feature.
Copying return True from a tutorial that was demonstrating suppress is the remaining footgun.
Default `exit to return False. Use contextlib.suppress(SpecificError) when silencing is the point. Empty suppress()` is a lint, not a shortcut.
`exit cannot resume the rest of the with` body. The stack already unwound. One r/learnpython thread tried to build on-error-resume-next this way, and it does not work.
@contextmanager objects and files after close are one-shot. The error is RuntimeError: generator didn't yield, which reads like a generator bug and is actually a reuse bug.
Create them in the header. Decorating works on ContextDecorator helpers because each call builds a new generator.
Reentrant is not thread-safe. redirect_stdout can nest. It still is not a lock.
Porteous: you intuitively treat a new indent as a new scope. Context managers do not create a new scope.
After with open(...) as f, f still exists. It is closed. A later f.read() is ValueError, not NameError.
open() opens immediately, even without with. A Lock acquires on enter. Read the object's docs before assuming when the resource is taken.
Calling `enter / exit yourself is a smell. You lose the assignment-failure guarantee and the exception triple. ExitStack.enter_context` is the supported escape hatch when enter has to be dynamic.

A Python decorator is a function that returns another function, usually applied with @wrapper syntax so the returned callable replaces the original name.

Scrapy is a crawl framework, not a parser. Learn the Engine loop, spiders, item pipelines, and when a requests + BeautifulSoup script still wins.

LlamaIndex for Python: RAG, agents, LlamaParse, LiteParse v2.1, vs LangChain, and the silent OpenAI fallback.