Python Context Managers: with Statement and contextlib
Context managers are Python's mechanism for deterministic resource cleanup. The with statement guarantees that __exit__ runs whether the block completes normally or raises an exception — eliminating the class of bugs caused by forgetting to close files, release locks, or roll back transactions. This guide covers the protocol, class-based and generator-based context managers, contextlib utilities, and async context managers.
Table of Contents
How the with Statement Works
The with statement calls __enter__ on entry and __exit__ on exit — even if an exception is raised inside the block. The value returned by __enter__ is bound to the as target. __exit__ receives the exception type, value, and traceback if an exception occurred, or None, None, None on success. Returning a truthy value from __exit__ suppresses the exception.
# What Python does under the hood for:
# with open("file.txt") as f:
# data = f.read()
mgr = open("file.txt")
f = mgr.__enter__()
try:
data = f.read()
except Exception as exc:
if not mgr.__exit__(type(exc), exc, exc.__traceback__):
raise
else:
mgr.__exit__(None, None, None)
# Multiple context managers on one line (Python 3.10+: parenthesized)
with (
open("input.txt") as fin,
open("output.txt", "w") as fout,
):
fout.write(fin.read().upper())
Class-Based Context Managers
Implement __enter__ and __exit__ to make any class usable as a context manager. This approach is best when the setup and teardown logic is complex or when you need to store state across the block.
import time
import logging
class Timer:
"""Context manager that measures elapsed time."""
def __init__(self, name=""):
self.name = name
def __enter__(self):
self.start = time.perf_counter()
return self # available as the 'as' target
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
label = f"[{self.name}] " if self.name else ""
print(f"{label}Elapsed: {self.elapsed:.4f}s")
return False # do not suppress exceptions
with Timer("data processing") as t:
total = sum(range(10_000_000))
print(f"Got {total}, took {t.elapsed:.4f}s")
class DatabaseTransaction:
"""Wraps a DB connection in a transaction, rolling back on error."""
def __init__(self, connection):
self.conn = connection
def __enter__(self):
self.conn.execute("BEGIN")
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.conn.execute("COMMIT")
else:
self.conn.execute("ROLLBACK")
logging.error("Transaction rolled back: %s", exc_val)
return False # re-raise any exception
@contextmanager Decorator
The contextlib.contextmanager decorator lets you write a context manager as a generator function. Code before yield is the setup (__enter__), the yielded value becomes the as target, and code after yield in a finally block is the teardown (__exit__). This is often more readable than a full class.
from contextlib import contextmanager
import tempfile
import os
@contextmanager
def temp_directory():
"""Create a temporary directory, clean it up on exit."""
import shutil
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
with temp_directory() as d:
path = os.path.join(d, "work.txt")
with open(path, "w") as f:
f.write("temporary work")
# d is automatically deleted here
@contextmanager
def change_directory(path):
"""Temporarily change the working directory."""
old = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old)
@contextmanager
def suppress_errors(*exceptions):
"""Context manager that silences specified exceptions."""
try:
yield
except exceptions:
pass
with suppress_errors(FileNotFoundError, PermissionError):
os.remove("/nonexistent/file.txt") # silently ignored
contextlib.suppress(*exceptions), which does the same as the custom suppress_errors above. Use it directly in production code.
contextlib Utilities
The contextlib module provides several ready-made context managers and helpers that solve common patterns without writing any boilerplate.
from contextlib import (
suppress, redirect_stdout, redirect_stderr,
ExitStack, nullcontext, contextmanager
)
import io
# suppress — ignore specific exceptions
with suppress(FileNotFoundError):
os.unlink("maybe_exists.tmp")
# redirect_stdout — capture print() output
buffer = io.StringIO()
with redirect_stdout(buffer):
print("captured output")
print("more output")
text = buffer.getvalue() # "captured output\nmore output\n"
# nullcontext — a no-op context manager, useful in conditional code
def process(data, lock=None):
ctx = lock if lock is not None else nullcontext()
with ctx:
return [x * 2 for x in data]
# ExitStack — manage a dynamic number of context managers
files_to_process = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [stack.enter_context(open(f)) for f in files_to_process]
for fh in handles:
print(fh.read())
# all files closed on exit regardless of errors
# ExitStack as a reusable cleanup manager
class Application:
def __init__(self):
self._stack = ExitStack()
def startup(self):
self._stack.enter_context(some_resource())
self._stack.callback(self._flush_logs)
def shutdown(self):
self._stack.close() # runs all registered cleanups
Async Context Managers
Async context managers implement __aenter__ and __aexit__ (both coroutines) and are used with async with. They are essential for managing async resources like database connections, HTTP client sessions, and async locks. Use @asynccontextmanager from contextlib for the generator style.
import asyncio
from contextlib import asynccontextmanager
import aiohttp
@asynccontextmanager
async def http_session():
"""Managed aiohttp ClientSession — ensures proper cleanup."""
async with aiohttp.ClientSession() as session:
yield session
async def fetch_users():
async with http_session() as session:
async with session.get("https://api.example.com/users") as resp:
return await resp.json()
# Async Timer
class AsyncTimer:
async def __aenter__(self):
self.start = asyncio.get_event_loop().time()
return self
async def __aexit__(self, *args):
self.elapsed = asyncio.get_event_loop().time() - self.start
print(f"Async block took {self.elapsed:.4f}s")
async def main():
async with AsyncTimer() as t:
await asyncio.sleep(0.1)
print(f"Elapsed: {t.elapsed:.4f}s")
asyncio.run(main())
Real-World Patterns
Context managers shine in production code for locking, tracing, feature flags, and test fixtures. Here are patterns you'll encounter in real codebases.
import threading
from contextlib import contextmanager
# Thread-safe counter using a lock as context manager
class Counter:
def __init__(self):
self._value = 0
self._lock = threading.Lock()
def increment(self):
with self._lock:
self._value += 1
@property
def value(self):
with self._lock:
return self._value
# Tracing / observability span
@contextmanager
def trace_span(name, tags=None):
import time
start = time.perf_counter()
try:
yield
except Exception as exc:
print(f"SPAN ERROR [{name}]: {exc}")
raise
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
print(f"SPAN [{name}] {elapsed_ms:.1f}ms tags={tags}")
with trace_span("db.query", tags={"table": "users"}):
# database call here
pass
# pytest fixture using contextmanager
@contextmanager
def fake_env(**kwargs):
"""Temporarily set environment variables in tests."""
import os
old = {k: os.environ.get(k) for k in kwargs}
os.environ.update({k: str(v) for k, v in kwargs.items()})
try:
yield
finally:
for k, v in old.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
with fake_env(DATABASE_URL="sqlite:///:memory:", DEBUG="true"):
# test code that reads env vars
pass
Frequently Asked Questions
- Can __exit__ suppress exceptions?
- Yes. If
__exit__returns a truthy value, the exception is suppressed and execution continues after thewithblock.contextlib.suppressworks this way. Use sparingly — silent exception swallowing is usually a bug. - What happens if __enter__ raises?
- If
__enter__raises,__exit__is NOT called because the context was never successfully entered. The exception propagates normally. - Can I use a context manager without the with statement?
- Yes, but don't do it in production. You can call
obj.__enter__()andobj.__exit__(None, None, None)manually, but this bypasses the safety guarantee and is error-prone. - How is contextmanager different from a class?
- Functionally identical. Use
@contextmanagerfor simple cases where the generator style reads more naturally. Use a class when you need to store state, inherit, or reuse the context manager across multiplewithblocks.