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.

Updated 10 min read
Python source code on a laptop screen

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.

Key Takeaways

  • A context manager is the protocol object (`enter / exit`), not "the thing that closes files."
  • with always calls `exit after a successful enter, including when the as` target assignment fails.
  • Returning a true value from `exit 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.

What Is a Python Context Manager?

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.

How the with Statement Works

The language reference runs with in a fixed order. Methods are loaded before `enter` runs (implicit special-method lookup).

  • Evaluate the context expression.
  • Load `enter and exit` from the result.
  • Call `enter. If that raises, exit` is not called.
  • Bind the as target if you wrote one.
  • Run the suite.
  • Call `exit(exc_type, exc_value, traceback), 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.

with Is Not a Plain finally

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.

Comma Form and Parentheses

Multiple with items nest: first entered, last exited. The comma form has been in the language since 3.1.

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

Types of Context Managers

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; @contextmanager instances

Reuse raises. Create the instance in the with header.

Reusable, not reentrant

Locks that deadlock on re-acquire

Successive with blocks on the same instance are fine. Nesting the same instance is not.

Reentrant

redirect_stdout, chdir, some decimal contexts

Nested with on the same instance is fine. Reentrant is not the same as thread-safe.

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

How to Write Your Own

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

Class Form

The protocol is two methods. Return False from `exit` unless you mean to swallow.

Python
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 False

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

The @contextmanager Decorator

@contextmanager shipped with contextlib in 2.5. It is a decorator that yields once: one yield, then teardown.

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

  • u/latkde in r/learnpython (May 2025) and Porteous prefer @contextmanager, because a hand-written `exit` is easy to get wrong.
  • Murphy prefers the class: the decorator looks short until you add 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.

contextlib Helpers You Will Actually Use

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

@contextmanager

2.5

Enter/exit are two linear chunks.

ContextDecorator

3.2

You want the same helper as with and as @decorator.

ExitStack

3.3

N resources, optional resources, or input-driven enter.

suppress

3.4

You intend to silence one specific error.

nullcontext

3.7

A branch might skip opening anything. Faster no-op than ExitStack.

closing

2.5 era

The object has .close() but is not a context manager.

redirect_stdout

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.

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

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

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

Async Context Managers

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.

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

Benefits of Context Managers

Cleanup You Cannot Forget on the Happy Path

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.

The Object Knows the Recipe

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

`exit` Can Inspect the Exception

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.

Challenges and Limitations

Accidental Exception Swallowing

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.

Single-Use Instances

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

Indentation Is Not a Scope

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.

Hand-Calling the Dunders

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.

Frequently Asked Questions

Related Articles