Python

Python Context Managers👨‍💻

Context managers are Python's answer to a problem every language struggles with: guaranteeing cleanup happens, no matter what. Every time you write with open(...), you're using one, but file handling barely scratches the surface. Database transactions that auto-rollback on failure, locks that always release, temporary state that always reverts -- these are the patterns where context managers earn their keep.

The mechanism is straightforward. A context manager is any object with __enter__ and __exit__ methods. The with statement calls __enter__ at the top, gives you the result, runs your block, then calls __exit__ -- even if your code throws. It's deterministic cleanup, like C++ RAII but explicit and composable. And with the @contextmanager decorator, you can write one in five lines instead of building a whole class.

Key Takeaways

  • 1The `with` statement guarantees `__exit__` runs even if the block raises an exception -- this is the whole point, not just syntactic sugar
  • 2`__enter__` returns the value bound by `as`; `__exit__` receives exception info and can suppress exceptions by returning `True`
  • 3The `@contextmanager` decorator from `contextlib` turns a generator function into a context manager -- everything before `yield` is setup, everything after is teardown
  • 4`contextlib` ships production-ready utilities: `suppress` for ignoring specific exceptions, `closing` for objects with `.close()`, `ExitStack` for dynamic cleanup, and `nullcontext` for conditional context managers
  • 5Async context managers (`__aenter__`/`__aexit__` or `@asynccontextmanager`) are essential for managing HTTP sessions, database connection pools, and other async resources
  • 6Nested `with` statements can be written on a single line with parenthesized syntax (Python 3.10+), and `ExitStack` handles cases where the number of managers is dynamic

Master python context managers

Take the Python Fundamentals course with hands-on lessons and challenges.

Examples

Database transaction that auto-rolls back on failure

python

This is the canonical use case beyond files. __exit__ inspects exc_type to decide between COMMIT and ROLLBACK. Returning False re-raises the exception so calling code can still handle it. Most ORMs (SQLAlchemy, Django) use exactly this pattern under the hood.

@contextmanager decorator -- write a context manager as a generator

python

Everything before yield is __enter__, the yield value is what 'as' binds, everything after yield is __exit__. The try/finally ensures cleanup runs even on exceptions. This is the preferred way to write simple context managers -- no class boilerplate needed.

ExitStack -- manage a dynamic number of resources

python

ExitStack solves the problem where you don't know at write time how many resources you need. It also lets you register arbitrary cleanup callbacks, not just context managers. Cleanup happens in LIFO order (last registered, first cleaned up).

Async context manager for HTTP sessions

python

Async context managers use __aenter__/__aexit__ (or @asynccontextmanager) and are invoked with 'async with'. They are essential in async Python because you cannot use blocking cleanup in an async context. Every HTTP client library (aiohttp, httpx) uses this pattern for session lifecycle.

suppress, closing, and nullcontext -- contextlib's greatest hits

python

These three utilities from contextlib cover the most common patterns. suppress replaces the noisy try/except/pass idiom. closing adapts legacy objects. nullcontext acts as a stand-in when a context manager is optional, avoiding if/else duplication of the managed block.

__exit__ exception handling -- suppressing vs propagating

python

The return value of __exit__ is the mechanism for exception suppression. Returning True swallows the exception; returning False (or nothing) lets it propagate. This is powerful but dangerous -- only suppress exceptions you genuinely expect and can handle. The contextlib.suppress utility is the safe, declarative version of this pattern.

Common Mistakes

Mistake:

Forgetting that `__exit__` must accept the three exception arguments (`exc_type`, `exc_val`, `exc_tb`) even if you don't use them

Fix:

Always define `__exit__(self, exc_type, exc_val, exc_tb)` with all three parameters. If your `__exit__` signature is wrong, Python raises a `TypeError` when the `with` block exits, which masks the original exception.

Mistake:

Returning `True` from `__exit__` without understanding the consequences -- this silently swallows ALL exceptions in the `with` block

Fix:

Return `False` (or return nothing) by default. Only return `True` for specific exception types you intentionally want to suppress. Silent exception swallowing is one of the hardest bugs to track down.

Mistake:

Using a bare `yield` in a `@contextmanager` function without wrapping it in `try/finally`, so cleanup is skipped if the `with` block raises

Fix:

Always wrap `yield` in `try/finally` inside a `@contextmanager`. The `yield` is where the user's code runs, and if it throws, your code after `yield` won't execute unless it's in a `finally` block.

Mistake:

Acquiring a resource in `__init__` instead of `__enter__` -- the resource is held even if the `with` block is never reached

Fix:

Do setup in `__enter__`, not `__init__`. The `__init__` method should only store configuration. Actual resource acquisition belongs in `__enter__` so the resource lifetime is tied to the `with` block, not the object lifetime.

Best Practices

  • Use `@contextmanager` for simple setup/teardown pairs -- reserve class-based context managers for when you need to store state across `__enter__` and `__exit__` or when the exception-handling logic in `__exit__` is non-trivial
  • Keep `__enter__` and `__exit__` fast. These methods run synchronously on every entry and exit, so expensive work (network calls, heavy I/O) should happen inside the managed block, not in the manager itself
  • Prefer `contextlib.suppress(SomeError)` over `try/except SomeError: pass` -- it signals intent more clearly and is harder to accidentally over-broaden
  • Use `ExitStack` when managing a variable number of resources or when you need to conditionally acquire resources inside a loop -- it handles partial-acquisition failures correctly by cleaning up everything acquired so far
  • Write context managers that compose well: return a useful value from `__enter__`, don't suppress exceptions unless you have a very specific reason, and document what cleanup guarantee you provide

Summary

Context managers pair setup with guaranteed teardown using `__enter__` and `__exit__`, invoked by the `with` statement. The `@contextmanager` decorator from `contextlib` lets you write one as a generator -- code before `yield` is setup, code after is teardown, and `try/finally` ensures cleanup on exceptions. For async resources, use `async with` and `@asynccontextmanager`. The `contextlib` module provides `suppress`, `closing`, `ExitStack`, and `nullcontext` for common patterns. The key rule: acquire resources in `__enter__`, release in `__exit__`, and let `with` handle the rest.

Practice Python with hands-on challenges

Learn python context managers hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Python with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.