Context Managers
Context manager patterns using with statements for reliable resource management in Python
You are an expert in Python context managers and the `with` statement for writing safe, idiomatic resource management code. ## Key Points - **The context manager protocol** requires `__enter__` (called on entry, return value bound by `as`) and `__exit__` (called on exit, receives exception info). - **`__exit__` receives** `(exc_type, exc_val, exc_tb)` — returning `True` suppresses the exception. - **Generator-based context managers** via `@contextlib.contextmanager` use a single `yield` to split setup and teardown. - **Async context managers** implement `__aenter__` / `__aexit__` or use `@contextlib.asynccontextmanager`. - **`contextlib.ExitStack`** composes a dynamic number of context managers at runtime. - Use `try/finally` inside generator-based context managers to guarantee cleanup even if the body raises. - Prefer `@contextmanager` for simple cases; use class-based managers when you need to store state across enter/exit or support reentrant usage. - Use `ExitStack` when the number of resources is not known at coding time. - Keep `__exit__` logic minimal — log or re-raise, but avoid complex branching. - Use `contextlib.suppress(ExceptionType)` instead of bare `try/except/pass` for clarity. - **Forgetting `finally`** in generator-based managers means cleanup is skipped on exceptions. - **Yielding more than once** in a `@contextmanager` raises `RuntimeError` — there must be exactly one yield.
skilldb get python-patterns-skills/Context ManagersFull skill: 172 linesContext Managers — Python Patterns
You are an expert in Python context managers and the with statement for writing safe, idiomatic resource management code.
Overview
Context managers provide a clean protocol for acquiring and releasing resources — files, locks, database connections, temporary state — guaranteeing cleanup even when exceptions occur. Python's with statement and the contextlib module make it straightforward to write and compose context managers.
Core Philosophy
Context managers encode a fundamental principle: resource acquisition and release should be inseparable. When you open a file, start a transaction, or acquire a lock, the cleanup logic belongs right next to the setup logic — not scattered across try/except/finally blocks three functions away. The with statement makes this contract visible and enforceable, ensuring that cleanup happens regardless of how the block exits.
The deeper insight is that context managers are about scope and invariants. They establish a temporary world — a database transaction, a changed directory, a held lock, a modified configuration — and guarantee that the world is restored when you leave. This is the same idea as RAII in C++ but expressed in Python's dynamic, protocol-driven style. When you design around context managers, you make impossible states impossible: you cannot forget to close a file, release a lock, or roll back a transaction.
Good context manager design follows the single-responsibility principle. Each context manager should manage exactly one resource or state transition. If you need to set up multiple resources, compose them with ExitStack or nested with statements rather than building a monolithic manager that juggles five things at once. This composability is what makes context managers scale from simple file handling to complex framework plumbing.
Core Concepts
- The context manager protocol requires
__enter__(called on entry, return value bound byas) and__exit__(called on exit, receives exception info). __exit__receives(exc_type, exc_val, exc_tb)— returningTruesuppresses the exception.- Generator-based context managers via
@contextlib.contextmanageruse a singleyieldto split setup and teardown. - Async context managers implement
__aenter__/__aexit__or use@contextlib.asynccontextmanager. contextlib.ExitStackcomposes a dynamic number of context managers at runtime.
Implementation Patterns
Class-based context manager
class DatabaseTransaction:
def __init__(self, connection):
self.connection = connection
def __enter__(self):
self.connection.begin()
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
self.connection.rollback()
else:
self.connection.commit()
return False # do not suppress exceptions
with DatabaseTransaction(conn) as db:
db.execute("INSERT INTO users ...")
Generator-based context manager
from contextlib import contextmanager
import time
@contextmanager
def timer(label="Block"):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label} took {elapsed:.4f}s")
with timer("Data load"):
data = load_large_dataset()
Temporary state change
from contextlib import contextmanager
import os
@contextmanager
def working_directory(path):
old_cwd = os.getcwd()
os.chdir(path)
try:
yield path
finally:
os.chdir(old_cwd)
with working_directory("/tmp/build"):
subprocess.run(["make", "all"])
ExitStack for dynamic resource management
from contextlib import ExitStack
def process_files(paths):
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
for f in files:
process(f.read())
Async context manager
from contextlib import asynccontextmanager
import aiohttp
@asynccontextmanager
async def http_session():
session = aiohttp.ClientSession()
try:
yield session
finally:
await session.close()
async def fetch(url):
async with http_session() as session:
async with session.get(url) as resp:
return await resp.json()
Reentrant and reusable context managers
from contextlib import contextmanager
@contextmanager
def managed_resource():
resource = acquire()
try:
yield resource
finally:
release(resource)
# Reusable: create a new instance each time
mgr = managed_resource # factory reference
with mgr() as r1:
...
with mgr() as r2:
...
Best Practices
- Use
try/finallyinside generator-based context managers to guarantee cleanup even if the body raises. - Prefer
@contextmanagerfor simple cases; use class-based managers when you need to store state across enter/exit or support reentrant usage. - Use
ExitStackwhen the number of resources is not known at coding time. - Keep
__exit__logic minimal — log or re-raise, but avoid complex branching. - Use
contextlib.suppress(ExceptionType)instead of baretry/except/passfor clarity.
Common Pitfalls
- Forgetting
finallyin generator-based managers means cleanup is skipped on exceptions. - Yielding more than once in a
@contextmanagerraisesRuntimeError— there must be exactly one yield. - Suppressing exceptions accidentally by returning
Truefrom__exit__when you meant to propagate. - Reusing a spent context manager — most generator-based managers are single-use; entering them a second time raises
RuntimeError. - Resource leaks in ExitStack if you call
enter_contextoutside thewithblock and an exception occurs before the stack is entered.
Anti-Patterns
-
Manual try/finally for standard resources — writing
f = open(path); try: ... finally: f.close()whenwith open(path) as fdoes the same thing with less code and fewer chances to forget the cleanup. Thewithform is always preferable for resources that support the protocol. -
God context manager — building a single context manager that opens a database connection, starts a transaction, acquires a lock, and sets up logging all at once. When any one of those steps fails mid-setup, the cleanup logic becomes a tangled mess. Compose single-purpose managers instead.
-
Suppressing exceptions silently — returning
Truefrom__exit__to swallow all exceptions without logging or re-raising. This hides bugs and makes debugging nearly impossible. Only suppress specific, expected exception types, and always document why. -
Stateful reuse of single-use managers — storing a generator-based context manager in a variable and entering it twice. The second entry raises
RuntimeErrorbecause the generator is already exhausted. Use a factory function that creates a fresh manager each time. -
Complex branching in exit — putting business logic, retry loops, or conditional commits inside
__exit__. Exit handlers should be simple and predictable: clean up, log if needed, and get out. Decision-making belongs in thewithblock body, not in the teardown.
Install this skill directly: skilldb add python-patterns-skills
Related Skills
Async Patterns
Asyncio patterns for concurrent I/O-bound programming in Python
Dataclasses
Dataclass and Pydantic model patterns for structured data in Python
Decorators
Decorator patterns for wrapping, extending, and composing Python functions and classes
Dependency Injection
Dependency injection patterns for loosely coupled, testable Python applications
Generators
Generator and itertools patterns for memory-efficient data processing in Python
Metaclasses
Metaclass and descriptor patterns for advanced class customization in Python