Skip to main content
Technology & EngineeringPython Patterns172 lines

Context Managers

Context manager patterns using with statements for reliable resource management in Python

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Context 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 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.

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/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.

Common Pitfalls

  • 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.
  • Suppressing exceptions accidentally by returning True from __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_context outside the with block 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() when with open(path) as f does the same thing with less code and fewer chances to forget the cleanup. The with form 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 True from __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 RuntimeError because 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 the with block body, not in the teardown.

Install this skill directly: skilldb add python-patterns-skills

Get CLI access →