Python Context Managers

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 finally block 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.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram