Writing a context manager the traditional way requires a class with __enter__ and __exit__ methods. For simple setup/teardown patterns, that is more structure than the problem demands. The @contextmanager decorator from contextlib lets you write the same thing as a generator function with a single yield. Code before the yield is the setup. Code after the yield is the teardown. The yielded value is what the as clause receives. This article covers the full contract of that yield, how exceptions flow through it, how to use the result as both a context manager and a function decorator, and when the class approach is still the better choice.
The Class Approach vs the Generator Approach
A traditional class-based context manager requires defining a class with two dunder methods. Here is a context manager that temporarily changes the working directory and restores it on exit:
import os
class ChangeDirectory:
def __init__(self, new_dir):
self.new_dir = new_dir
self.old_dir = None
def __enter__(self):
self.old_dir = os.getcwd()
os.chdir(self.new_dir)
return self.new_dir
def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.old_dir)
return False # do not suppress exceptions
with ChangeDirectory("/tmp") as path:
print(os.getcwd()) # /tmp
print(os.getcwd()) # original directory
The same behavior expressed with @contextmanager:
from contextlib import contextmanager
import os
@contextmanager
def change_directory(new_dir):
old_dir = os.getcwd()
os.chdir(new_dir)
try:
yield new_dir
finally:
os.chdir(old_dir)
with change_directory("/tmp") as path:
print(os.getcwd()) # /tmp
print(os.getcwd()) # original directory
The generator version is 8 lines instead of 14. The setup (os.chdir(new_dir)) appears before the yield. The teardown (os.chdir(old_dir)) appears after the yield in the finally block. The yielded value (new_dir) is what the as clause receives. No class, no self, no exc_type/exc_val/exc_tb parameters to manage.
The Yield Contract
The generator function decorated with @contextmanager must yield exactly once. The yield divides the function into two phases:
@contextmanager
def blueprint():
# ---- SETUP PHASE (__enter__) ----
# Acquire resources, configure state, open connections.
# This runs when the 'with' block is entered.
yield value # 'value' is bound to the 'as' variable.
# The with block body executes here.
# ---- TEARDOWN PHASE (__exit__) ----
# Release resources, restore state, close connections.
# This runs when the 'with' block exits.
If the generator yields zero times, @contextmanager raises RuntimeError("generator didn't yield"). If it yields more than once, it raises RuntimeError("generator didn't stop"). This is a strict contract: one yield, always.
What you yield determines what as receives. If you yield nothing (yield with no value), the as variable receives None. This is fine when the context manager manages state but does not produce a resource for the caller to use:
import sys
from contextlib import contextmanager
from io import StringIO
@contextmanager
def suppress_stdout():
"""Redirect stdout to /dev/null for the duration of the block."""
old_stdout = sys.stdout
sys.stdout = StringIO()
try:
yield # nothing to give the caller
finally:
sys.stdout = old_stdout
with suppress_stdout():
print("This will not appear")
print("This will appear")
Under the hood, @contextmanager creates a _GeneratorContextManager object. Its __enter__ calls next() on the generator to advance it to the yield. Its __exit__ either calls next() again (if no exception) or generator.throw(exception) (if an exception occurred in the with block).
Exception Handling
When an exception occurs inside the with block, @contextmanager throws it into the generator at the yield point. This means you can catch, log, suppress, or re-raise exceptions using standard try/except syntax.
Guaranteed Cleanup With try/finally
Wrapping the yield in try/finally ensures teardown runs regardless of whether an exception occurs:
@contextmanager
def database_transaction(connection):
cursor = connection.cursor()
try:
yield cursor
connection.commit()
except Exception:
connection.rollback()
raise
finally:
cursor.close()
If the with block completes without error, the code after yield calls commit(). If an exception occurs, the except block calls rollback() and re-raises the exception. Either way, the finally block closes the cursor.
Suppressing Exceptions
If the generator catches an exception and does not re-raise it, the exception is suppressed. This is the generator equivalent of returning True from __exit__:
import logging
from contextlib import contextmanager
logger = logging.getLogger(__name__)
@contextmanager
def log_and_suppress(*exception_types):
"""Log exceptions of the given types and suppress them."""
try:
yield
except exception_types as e:
logger.error("Suppressed %s: %s", type(e).__name__, e)
# Not re-raising => exception is suppressed
with log_and_suppress(ValueError, TypeError):
int("not a number") # ValueError is logged and suppressed
print("Execution continues here")
If you write a bare yield without a try/finally and an exception occurs in the with block, the teardown code after yield will not execute. Always wrap yield in try/finally when cleanup must be guaranteed.
Practical Patterns
Timing a Code Block
import time
from contextlib import contextmanager
@contextmanager
def timer(label="block"):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"[{label}] {elapsed:.6f}s")
with timer("sorting"):
data = sorted(range(1_000_000, 0, -1))
# [sorting] 0.072345s
Temporarily Overriding an Environment Variable
import os
from contextlib import contextmanager
@contextmanager
def env_var(name, value):
"""Temporarily set an environment variable, restoring on exit."""
original = os.environ.get(name)
os.environ[name] = value
try:
yield value
finally:
if original is None:
del os.environ[name]
else:
os.environ[name] = original
with env_var("API_KEY", "test-key-123") as key:
print(os.environ["API_KEY"]) # test-key-123
# API_KEY is restored to its original value (or removed)
Managed Database Connection
import sqlite3
from contextlib import contextmanager
@contextmanager
def db_connection(db_path):
conn = sqlite3.connect(db_path)
try:
yield conn
except Exception:
conn.rollback()
raise
else:
conn.commit()
finally:
conn.close()
with db_connection(":memory:") as conn:
conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'Alice')")
row = conn.execute("SELECT * FROM users").fetchone()
print(row) # (1, 'Alice')
# Connection is committed and closed automatically
Using @contextmanager as a Function Decorator
Since Python 3.2, context managers created with @contextmanager inherit from ContextDecorator, which means they can be applied directly as function decorators. When used this way, the entire function body runs inside the context manager's with block:
from contextlib import contextmanager
import time
@contextmanager
def timer(label="function"):
start = time.perf_counter()
try:
yield
finally:
print(f"[{label}] {time.perf_counter() - start:.6f}s")
# Used as a context manager
with timer("inline"):
sum(range(1_000_000))
# Used as a function decorator
@timer("decorated")
def compute():
return sum(range(1_000_000))
compute()
# [inline] 0.012345s
# [decorated] 0.012678s
When used as a decorator, the yielded value is not accessible because there is no as clause. This makes the decorator form ideal for context managers that manage state (like timing or logging) but do not need to hand a resource to the caller.
Async Context Managers
For async with statements, contextlib provides @asynccontextmanager, which works identically to @contextmanager but with an async generator:
from contextlib import asynccontextmanager
@asynccontextmanager
async def managed_session(url):
session = await create_session(url)
try:
yield session
finally:
await session.close()
# Usage:
async def fetch_data():
async with managed_session("https://api.example.com") as session:
return await session.get("/data")
The async version was added in Python 3.7 and gained decorator support in Python 3.10.
When to Use a Class Instead
The @contextmanager decorator excels at simple setup/teardown pairs. A class-based context manager is the better choice when:
| Scenario | Use @contextmanager | Use a Class |
|---|---|---|
| Simple setup/teardown pair | Yes | Overkill |
Need to return self from __enter__ | Awkward | Natural |
| Need state accessible after the with block | Requires a separate data object | Store on self |
| Complex __exit__ logic with exc_type checks | Less clear | Explicit parameters |
| Context manager is reusable across multiple with blocks | No (single-use) | Yes (if designed for it) |
| Want dual use as decorator | Built-in since 3.2 | Inherit from ContextDecorator |
Context managers created with @contextmanager are single-use. If you try to enter the same context manager a second time, the generator has already been exhausted and Python raises RuntimeError. Class-based context managers can be designed for reuse by resetting state in __enter__.
Key Takeaways
@contextmanagerconverts a generator function into a context manager. Code beforeyieldis the setup (__enter__). Code afteryieldis the teardown (__exit__). The yielded value becomes theasvariable.- The generator must yield exactly once. Zero yields produces
RuntimeError("generator didn't yield"). More than one yield producesRuntimeError("generator didn't stop"). - Always wrap
yieldintry/finallyfor guaranteed cleanup. Withouttry/finally, an exception in the with block prevents the teardown code from executing. Thefinallyblock ensures cleanup runs regardless. - Exception handling uses standard
try/exceptsyntax. If the generator catches an exception and does not re-raise it, the exception is suppressed. If it re-raises or does not catch it, the exception propagates normally. - Since Python 3.2, the result works as both a context manager and a function decorator. When used as a
@decorator, the entire function body runs inside the context. The yielded value is not accessible in decorator mode. - Use a class when the context manager needs persistent state, reuse, or complex exit logic.
@contextmanagerproduces single-use context managers that cannot be entered more than once. If you needself, reusability, or explicit exception-type inspection in__exit__, write a class.
@contextmanager is one of the standard library's most useful abstractions. It turns a pattern that requires a class with two dunder methods into a single function with a yield. For the majority of context management tasks -- temporary state changes, resource acquisition/release, timing, logging wrappers -- the generator approach is shorter, clearer, and sufficient.