A Python context manager controls setup and cleanup around a block of code. The with statement enters the context, runs the block, and exits the context even when the block raises an exception.
Files, locks, database transactions, temporary directories, and redirected output commonly use this pattern. Context managers make resource ownership visible and prevent cleanup logic from being scattered across several return and error paths.
Use a context manager when an operation has a clear acquire-use-release lifecycle. It does not replace ordinary functions when no cleanup or temporary state is involved.
Use the with Statement
Python file objects implement the context manager protocol. Leaving the block closes the file automatically.
Example:
# Open the report and close it after the block
with open("sales.txt", "r", encoding="utf-8") as report:
first_line = report.readline()
print(first_line)
Several context managers can share one with statement. Python exits them in reverse order, matching nested blocks.
# Open the input first and close the output first
with (
open("orders.csv", encoding="utf-8") as source,
open("orders-copy.csv", "w", encoding="utf-8") as target,
):
target.write(source.read())
Understand the Context Manager Protocol
A synchronous context manager defines __enter__() and __exit__(). Enter returns the value assigned after as. Exit receives the exception type, exception value, and traceback, or three None values after normal completion.
class Timer:
def __enter__(self):
# Record setup state and expose this instance
self.started = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
# Cleanup runs for normal and exceptional exits
self.elapsed = time.perf_counter() - self.started
print(f"Elapsed: {self.elapsed:.4f}s")
return False # Do not suppress exceptions
Control Exception Suppression
If __exit__() returns a truthy value, Python suppresses an exception raised inside the block. Returning false or None lets it propagate. Suppress only errors that the context manager can handle completely.
class IgnoreMissingFile:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
# Suppress only FileNotFoundError
return exc_type is FileNotFoundError
with IgnoreMissingFile():
Path("temporary.log").unlink()
Create a Generator-Based Context Manager
The @contextmanager decorator converts a generator function into a context manager. Code before yield enters the context, the yielded value is bound by as, and the finally block performs cleanup.
from contextlib import contextmanager
from pathlib import Path
import os
@contextmanager
def working_directory(path):
# Save state before changing it
previous = Path.cwd()
os.chdir(path)
try:
yield Path.cwd()
finally:
# Restore state even when the block fails
os.chdir(previous)
with working_directory("/tmp") as current:
print(current)
The decorated generator must yield exactly once. If an exception enters at the yield point, re-raise it unless the manager deliberately and safely handles it.
Use contextlib Utilities
| Utility | Purpose |
|---|---|
| closing() | Call close() on an object lacking protocol support |
| suppress() | Ignore specified exception types |
| nullcontext() | Provide an optional no-op context |
| redirect_stdout() | Temporarily redirect standard output |
| chdir() | Temporarily change the working directory |
| ExitStack | Manage a dynamic number of contexts |
Use suppress() for narrow, expected errors, not broad Exception handling that hides programming failures.
from contextlib import suppress
# Missing cleanup files are acceptable in this operation
with suppress(FileNotFoundError):
Path("cache.tmp").unlink()
Manage Dynamic Resources with ExitStack
ExitStack enters a variable number of context managers and calls their exits in reverse order. If opening a later resource fails, resources already opened are still released.
from contextlib import ExitStack
filenames = ["north.txt", "south.txt", "west.txt"]
with ExitStack() as stack:
# Every successfully opened file joins the cleanup stack
files = [
stack.enter_context(open(name, encoding="utf-8"))
for name in filenames
]
totals = [file.readline().strip() for file in files]
Use stack.callback() to register a cleanup function that does not need exception details. pop_all() transfers the pending cleanup operations when acquisition succeeds and ownership must continue beyond the block.
Create an Asynchronous Context Manager
An asynchronous context manager defines __aenter__() and __aexit__() and is used with async with. The @asynccontextmanager decorator offers a generator-style alternative.
from contextlib import asynccontextmanager
@asynccontextmanager
async def connection(pool):
# Await asynchronous acquisition
client = await pool.acquire()
try:
yield client
finally:
# Always return the client to the pool
await pool.release(client)
async def load_customer(pool):
async with connection(pool) as client:
return await client.fetch_customer(1042)
Reentrancy and Reuse
Some context managers can be reused or nested; others represent one-shot state. Generator-based manager instances are one-shot, although calling the decorated factory creates a fresh instance. Document these expectations when exposing a custom manager.
Common Mistakes
- Acquiring a resource before entering the context manager.
- Forgetting a
finallyblock around generator cleanup. - Returning true from
__exit__()unintentionally. - Suppressing broad exception classes.
- Sharing a non-reentrant manager across nested calls.
- Using synchronous cleanup for asynchronous resources.
Conclusion
Python context managers make setup, cleanup, and temporary state reliable. Use with for existing resources, implement the protocol or @contextmanager for custom lifecycles, use ExitStack for dynamic resources, and adopt asynchronous variants when cleanup must be awaited. Clear ownership produces safer, easier-to-read code.