Python __exit__ Returning True — The Silent Bug Pattern
return True in __exit__ caused 5% data loss by swallowing IntegrityError silently.- Context managers encode the cleanup contract at the resource, not the caller.
- __enter__ and __exit__ give you full control over resource lifecycle and exception handling.
- contextlib.contextmanager reduces boilerplate but requires strict one-yield and try/finally.
- Context managers wrap resource setup/teardown into a reusable with statement
- __enter__ returns the resource; __exit__ is always called — even on exceptions
- Return True from __exit__ to suppress exceptions; return False (default) to propagate
- Performance: __enter__/__exit__ overhead ~100ns; real cost is in your cleanup logic
- contextlib.contextmanager turns a generator into a context manager — yield exactly once
- Production trap: returning True for unknown exception types hides bugs; always log then re-raise
Quick Debug Cheat Sheet: Context Managers
Resource not released after with block
python -c "from io.thecodeforge.contextmanager import FileManager; with FileManager('test.txt') as f: raise Exception('test')"Check for __del__ method (not a guarantee, but risky)Exception suppressed – no traceback
grep -rn 'def __exit__' src/Add temporary logging: '__exit__ called with exc_type=%s, exc_val=%s, exc_tb=%s'Generator-based context manager resumes after yield – unexpected value
Trace the generator: import traceback; traceback.print_stack()Check for extra yields in the generator body after the main yield.Production Incident
Production Debug GuideDiagnose the most common context manager failures in production
throw(). Ensure your generator can handle being throw()n into. Use try/finally inside the generator.Resource leaks don't crash your program immediately. They accumulate silently until your server runs out of file descriptors at 3 AM on a Friday. Context managers exist to close the gap between 'I opened a resource' and 'I definitely cleaned it up'.
The problem they solve is the try/finally boilerplate that every experienced developer has written a hundred times. Without context managers, safe resource handling means nesting logic inside explicit try blocks and writing finally clauses that duplicate teardown across your codebase. Context managers encode that contract once — in the resource itself — and then you use the clean with statement everywhere.
You'll learn exactly what CPython does when it encounters a with statement, how to build context managers as classes and generator-based decorators, how exception suppression works at the bytecode level, how to compose multiple managers correctly, and the production gotchas that bite even seasoned Python engineers. This goes well past the with open() example.
What is a Context Manager?
Context managers are Python's way to guarantee resource cleanup. When you write with open('file.txt') as f:, Python calls __enter__ on the file object, binds the return value to f, runs your block, and then calls __exit__ regardless of how the block exits. That's the bedrock. Without it, every resource acquisition becomes a manual try/finally dance. Context managers move that boilerplate from the call-site to the resource itself.
Here's a minimal class-based context manager that wraps a file handle. Notice the __exit__ signature: it receives three arguments even when no exception occurs.
class ManagedFile: def __init__(self, filename: str, mode: str = 'r'): self._filename = filename self._mode = mode self._file = None def __enter__(self): self._file = open(self._filename, self._mode) return self._file def __exit__(self, exc_type, exc_val, exc_tb): if self._file: self._file.close() return False # propagate exceptions # Usage with ManagedFile('data.txt', 'w') as f: f.write('hello')
close() or release() — not just returns True.How __enter__ and __exit__ Work Internally
Every Python context manager relies on two magic methods. When you call with obj:, CPython first invokes obj.__enter__() and binds the return value to the variable after as. After the block completes—whether normally or via exception—it calls obj.__exit__(exc_type, exc_val, exc_tb). The return value of __exit__ determines if exceptions propagate: return True to suppress, False or None to propagate.
Here's a skeleton implementation for a file-like resource. Notice that __exit__ receives three positional arguments, and if you omit one, Python raises a TypeError at runtime — not at definition time. Many teams discover this in production when an unexpected exception triggers the else branch.
import io class ManagedFile: def __init__(self, filename: str, mode: str = 'r'): self._filename = filename self._mode = mode self._file = None def __enter__(self): self._file = open(self._filename, self._mode) return self._file def __exit__(self, exc_type, exc_val, exc_tb): if self._file: self._file.close() # Do not suppress exceptions return False # Usage with ManagedFile('data.txt', 'w') as f: f.write('hello')
Exception Handling in Context Managers
The real power of context managers lies in exception handling. The __exit__ method receives the exception type, value, and traceback. You can inspect them and either re-raise (by returning False), suppress (by returning True), or transform the exception. Common patterns include logging, cleanup on errors, and converting one exception to another.
For instance, you might want to wrap a low-level IOError into a custom NetworkError. The key pitfall: if you raise a new exception inside __exit__ while an exception is already active, Python 3.7+ sets the new exception's __context__ to the original, allowing chained debugging. But if you raise a new exception when no exception occurred (clean exit), that new exception simply propagates. Test both paths.
class DatabaseConnection: def __init__(self, connection_string: str): self._conn_string = connection_string self._conn = None def __enter__(self): print(f"Connecting to {self._conn_string}") self._conn = ... # real connection logic return self._conn def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: import logging logging.warning(f"Database error occurred: {exc_val}") raise DatabaseError(f"Dependency failed: {exc_val}") from exc_val self._conn.close() return False
Using contextlib for Simpler Context Managers
Writing a class with __enter__ and __exit__ is explicit but verbose. Python's contextlib module provides the @contextmanager decorator that turns a generator function into a context manager. The generator yields exactly once — that's the execution point where the with block runs. Setup goes before yield; teardown goes after yield. Exceptions are injected via .generator.throw()
This approach reduces boilerplate and makes the resource lifecycle more readable. But beware: the generator must yield exactly once. If it yields twice, a RuntimeError is raised. Also, if the managed block raises an exception that the generator catches but then raises a different exception, the second exception propagates and the first is lost — unless you chain it. Always use try/finally around the yield to guarantee teardown.
from contextlib import contextmanager @contextmanager def managed_file(filename: str, mode: str = 'r'): file = None try: file = open(filename, mode) yield file finally: if file: file.close() with managed_file('data.txt', 'w') as f: f.write('hello')
- Setup code before yield runs every time the with statement is entered.
- The yield value becomes the as target.
- Teardown code after yield runs when the block exits — even if an exception occurred.
- If an exception occurs, it is thrown into the generator at the yield point.
- Always wrap the yield in try/finally to ensure teardown runs regardless.
Nested Context Managers and Advanced Patterns
Real-world code often needs multiple context managers. You can nest with statements, but that becomes messy when the number grows. Python 3.1 introduced with A as a, B as b:, but for dynamic collections, contextlib.ExitStack is the right tool. ExitStack lets you manage multiple context managers as a stack: you push entries, and they are cleaned up in reverse order (LIFO) when the stack exits.
: temporarily ignore specific exceptions.suppress()redirect_stdout/stderr: redirect streams (useful in tests).: a no-op context manager for conditional resource handling.nullcontext(): wraps a closeable object.closing()
One less-known trap: if one of multiple comma-separated context managers raises during __enter__, all already-opened managers are still cleaned up. But if you're not using ExitStack, the order of cleanup is reverse of entry. ExitStack makes that explicit.
from contextlib import ExitStack, contextmanager @contextmanager def managed_connection(db_name: str): print(f"Opening {db_name}") yield f"conn_{db_name}" print(f"Closing {db_name}") with ExitStack() as stack: conns = [stack.enter_context(managed_connection(f"db{i}")) for i in range(3)] print(f"All connections open: {conns}") # After the with block, each connection is closed in reverse order.
Opening db1
Opening db2
All connections open: ['conn_db0', 'conn_db1', 'conn_db2']
Closing db2
Closing db1
Closing db0
Testing Context Managers for Production Reliability
Context managers are easy to test incorrectly. Most unit tests only cover the happy path: enter, do work, exit. The tricky parts are exception paths and cleanup guarantees. You should inject exceptions at every stage: during __enter__, inside the managed block, and during __exit__. Use pytest fixtures and monkeypatch to simulate failures.
Here's a test pattern that exercises all three failure points. The most insidious test gap is when __exit__ itself raises while an exception is already active. CPython 3.7+ converts that to a new exception with the original in __context__, but many teams miss this because they don't test dual-exception scenarios.
import pytest from io.thecodeforge.context_manager import ManagedFile def test_context_manager_exception_during_block(): with pytest.raises(ValueError): with ManagedFile('/tmp/test.txt', 'w') as f: raise ValueError("Simulated error") # After the block, the file should be closed import os # Check file descriptor (simplified) assert True # In real test, verify close was called def test_context_manager_exception_during_exit(monkeypatch): def failing_close(): raise OSError("Close failed") with ManagedFile('/tmp/test2.txt', 'w') as f: monkeypatch.setattr(f, 'close', failing_close) # __exit__ should not suppress the OSError # In practice, this will raise OSError when exiting with block # This test is for illustration pass
Async Context Managers: __aenter__ and __aexit__
Python 3.5 introduced async context managers for use with async with. They follow the same pattern but with coroutines: __aenter__ and __aexit__ are async methods that return awaitable objects. The @contextlib.asynccontextmanager decorator works analogously for async generators.
Async context managers are essential for managing resources in asynchronous code — database connections, aiohttp sessions, file handles in asyncio. The cleanup guarantees are the same as sync managers: __aexit__ is always called, even if the async block raises an exception. A common mistake: forgetting to make __aexit__ a coroutine, which results in a RuntimeError. Another: performing blocking I/O inside __aexit__ without awaiting, which stalls the event loop.
import asyncio from contextlib import asynccontextmanager class AsyncDatabaseConnection: def __init__(self, dsn: str): self._dsn = dsn self._conn = None async def __aenter__(self): self._conn = await connect_to_db(self._dsn) return self._conn async def __aexit__(self, exc_type, exc_val, exc_tb): if self._conn: await self._conn.close() return False # propagate exceptions @asynccontextmanager async def managed_session(dsn: str): conn = await connect_to_db(dsn) try: yield conn finally: await conn.close() async def example(): async with AsyncDatabaseConnection("postgres://...") as conn: await conn.execute("SELECT 1") # conn is closed
async keyword before with, Python raises a SyntaxError. Also, __aexit__ must be a coroutine — returning a plain value (like False) works, but you cannot use raise directly without await if you need to await another async cleanup.| Approach | Boilerplate | Exception Control | Use Case |
|---|---|---|---|
| Class with __enter__/__exit__ | More verbose – full class | Full control – inspect, suppress, transform | Complex resources needing custom state |
| Generator with @contextmanager | Minimal – single function | Limited – exception arrives at yield, you can handle in try/finally | Simple setup/teardown, single resource |
| contextlib.suppress() | One-line wrapper | Suppresses specific exception types | Ignoring expected errors (e.g., FileNotFoundError when deleting) |
| ExitStack | Dynamic push/pop | Stack-level cleanup; individual manager exceptions propagate | Managing groups of dynamic resources |
| Async class with __aenter__/__aexit__ | More verbose – full async class | Full async control | Async resources (DB, HTTP sessions) |
| Async generator with @asynccontextmanager | Minimal – single async function | Limited – same as sync generator | Simple async setup/teardown |
🎯 Key Takeaways
- Context managers encode the cleanup contract at the resource, not the caller.
- __enter__ and __exit__ give you full control over resource lifecycle and exception handling.
- contextlib.contextmanager reduces boilerplate but requires strict one-yield and try/finally.
- ExitStack handles dynamic resource collections; cleanup order is always LIFO.
- Test context managers with exception injection — happy-path testing is not enough.
- Returning True from __exit__ suppresses exceptions; most of the time you want False.
- Async context managers use __aenter__/__aexit__; same rules apply for suppression.
- Log any suppressed exceptions at WARNING level to avoid silent data loss.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is a context manager in Python and why would you use one?JuniorReveal
- QExplain how exception suppression works in context managers. When would you want to suppress an exception?Mid-levelReveal
- QHow does contextlib.contextmanager work under the hood? What are its limitations?SeniorReveal
- QWhat is ExitStack and when should you use it instead of nested with statements?SeniorReveal
- QWhat happens if __exit__ itself raises an exception? Does the original exception get lost?SeniorReveal
- QHow would you create a context manager for a database transaction that commits on success and rolls back on failure?Mid-levelReveal
Frequently Asked Questions
What is a context manager in Python in simple terms?
Think of a context manager as a wrapper around a resource that ensures setup happens before you use it and cleanup happens after, no matter what. The with statement is how you invoke it.
Can I use a context manager without a class?
Yes, use the @contextmanager decorator from the contextlib module. It turns a generator function into a context manager, reducing boilerplate.
What happens if I return True from __exit__ without handling the exception?
The exception is suppressed — the program continues as if nothing happened. This is dangerous; you should only suppress exceptions you've explicitly handled and logged.
What is the difference between contextlib.suppress and a try/except?
contextlib.suppress is a context manager that suppresses specific exceptions within its block. It's syntactic sugar for a try/except with pass, but it only suppresses the listed exceptions, not all.
How do I use multiple context managers in one with statement?
Use comma separation: with open('a') as a, open('b') as b:. Python 3.1+ allows this. For an unknown number, use ExitStack.
What should I do if my context manager fails to clean up when an exception occurs?
Check that __exit__ is called correctly and doesn't suppress the exception. Use a finally block or ensure __exit__ always performs cleanup before returning. Add logging and test with exception injection.
How do async context managers differ from sync ones?
Async context managers use __aenter__ and __aexit__ coroutines instead of regular methods. They are used with async with. The same exception handling rules apply, but you must remember to make __aexit__ a coroutine and await any async cleanup.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.