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.
Master python context managers
Take the Python Fundamentals course with hands-on lessons and challenges.
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.
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 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 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.
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.
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.
Forgetting that `__exit__` must accept the three exception arguments (`exc_type`, `exc_val`, `exc_tb`) even if you don't use them
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.
Returning `True` from `__exit__` without understanding the consequences -- this silently swallows ALL exceptions in the `with` block
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.
Using a bare `yield` in a `@contextmanager` function without wrapping it in `try/finally`, so cleanup is skipped if the `with` block raises
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.
Acquiring a resource in `__init__` instead of `__enter__` -- the resource is held even if the `with` block is never reached
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.
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.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.