Python __exit__ Returning True — The Silent Bug Pattern
A single return True in __exit__ caused 5% data loss by swallowing IntegrityError silently.
20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.
- 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
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.
Imagine you borrow a library book. The librarian checks it out to you, you read it, and when you're done — whether you finished it, spilled coffee on it, or had an emergency — the librarian takes it back and stamps it returned. You never have to remember to return it yourself. A Python context manager is that librarian: it sets something up before you need it, and guarantees it gets cleaned up after you're done, no matter what goes wrong in between.
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
When to Return True in __exit__ — Expected vs Unexpected Suppression
Returning True from __exit__ is a sharp tool. It suppresses exceptions, meaning your code continues as if nothing happened. Use it only when you are certain that the exception is both expected and harmless. Common legitimate use cases include:
- Cleanup that should not fail: If a resource is already closed or released, attempting to close it again may raise an OSError. You can safely suppress that because the resource is already in the desired state.
- Using contextlib.suppress: This is the idiomatic way to ignore known, safe exceptions in a localized block. For example, ignoring FileNotFoundError when deleting a file that may or may not exist.
- Exception during rollback: In a database transaction, if rollback itself raises (e.g., connection lost), you may choose to suppress it because the transaction is already aborted. But you must log it.
Never suppress exceptions you do not fully understand or expect. The silent data loss incident described earlier is a direct consequence of returning True for IntegrityError—an exception that signals data corruption. Always log suppressed exceptions at WARNING level at minimum.
Here's a decision tree to help decide:
import logging class SafeFileCleanup: """Context manager that safely suppresses expected close failures.""" def __init__(self, filename: str): self._file = open(filename, 'w') def __enter__(self): return self._file def __exit__(self, exc_type, exc_val, exc_tb): try: self._file.close() except OSError as e: # Known expected failure if file was already closed externally logging.warning(f"Close failed for {self._file.name}: {e}") return True # suppress this specific OSError return False # let other exceptions propagate # Example of BAD suppression: class BadSupress: def __exit__(self, exc_type, exc_val, exc_tb): # This swallows every exception, including bugs return True
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.
Managing Multiple Resources with ExitStack
When you need to work with an unknown number of resources—like opening all files in a directory or establishing connections based on a runtime configuration—ExitStack is the canonical solution. It manages a stack of entered contexts and guarantees LIFO cleanup even if one of the enter calls fails.
Here's a real‑world example: a configuration‑driven database migration tool that connects to multiple databases. The number of databases is read from a config file, so you cannot hard‑code with statements. ExitStack lets you push each database connection context manager dynamically.
A common production use case is handling partial failures during entry. If the third connection fails, ExitStack properly closes the first two connections. Without ExitStack, you'd need a manual try/finally cascade that grows with the number of resources.
from contextlib import ExitStack import json def run_migrations(config_path: str): with open(config_path) as f: config = json.load(f) dbs = config['databases'] with ExitStack() as stack: connections = [] for db_config in dbs: # enter_context returns the context manager's __enter__ return value conn = stack.enter_context( create_db_connection(db_config['host'], db_config['port']) ) connections.append(conn) print(f"Connected to {db_config['name']}") for conn in connections: conn.run_migration() # stack closes connections in reverse order upon exit
stack.enter_context() inside an if block to only open a resource when needed.stack.push() vs stack.enter_context(): push() adds an already‑entered context manager to the cleanup stack, while enter_context() both enters and pushes.enter_context() to ensure proper initialization.enter_context() to both open and register for cleanup.contextlib Quick Reference Table: @contextmanager and ExitStack
The @contextmanager decorator and ExitStack are two of the most powerful tools in contextlib. This table provides a quick comparison to help you choose the right one for your situation.
| Feature | @contextmanager | ExitStack |
|---|---|---|
| Purpose | Turn a generator into a single context manager | Manage a dynamic stack of multiple context managers |
| Setup/Teardown | Code before yield = setup; code after yield = teardown | Push contexts via , cleanup is automatic LIFO |
| Number of Resources | Exactly one resource per generator | Unlimited; dynamic at runtime |
| Exception Control | Exception thrown into generator at yield; you handle with try/except | Each individual context manager handles its own exceptions; overall suppression controlled by ExitStack's __exit__ |
| Limitations | Must yield exactly once; tricky exception chaining | Slightly more verbose; must be careful with vs |
| Use Case | Single resource with simple setup/teardown | Multiple or conditionally opened resources |
Both have their place. Use @contextmanager when you need a quick wrapper for one resource. Use ExitStack when you need to manage a variable number of resources, especially when the set is not known until runtime.
Below is a code snippet illustrating a simple @contextmanager usage and an ExitStack usage side by side.
from contextlib import contextmanager, ExitStack # @contextmanager example @contextmanager def simple_resource(name: str): print(f"Acquire {name}") yield name print(f"Release {name}") with simple_resource("file1") as r: print(f"Using {r}") # ExitStack example with ExitStack() as stack: resources = [stack.enter_context(simple_resource(f"file{i}")) for i in range(3)] print(f"Using {resources}") # Output shows LIFO cleanup
pop_all() method can move contexts into a broader scope when needed.Reentrant Context Managers
A reentrant context manager is one that can be entered multiple times, even while already inside a with block using the same manager instance. The typical example is threading.Lock — you can use a lock with with and re‑enter the same lock if it already holds it? Actually, threading.Lock is not reentrant; threading.RLock (reentrant lock) is: if a thread owns an RLock, it can acquire it again without deadlocking. But the term “reentrant context manager” in the context of the with statement means the same object can be used as a context manager multiple times, possibly nested.
Most context managers are non‑reentrant — entering twice (even without explicit nesting) leads to undefined behavior or errors. For example, a file object: if you call with f:, then try to enter another with f: inside the first block, Python will raise ValueError: I/O operation on closed file. because the first __exit__ already closed the file. Reentrant context managers are rare but useful for certain patterns like resource pools or retry logic.
Here's a reentrant context manager that uses a counter to allow nested usage without double‑closing.
import contextlib class ReentrantResource: def __init__(self): self._resource = "resource_handler" self._enter_count = 0 def __enter__(self): self._enter_count += 1 if self._enter_count == 1: # Acquire underlying resource only once print("Acquiring resource") return self._resource def __exit__(self, exc_type, exc_val, exc_tb): self._enter_count -= 1 if self._enter_count == 0: # Release only when all nested uses are done print("Releasing resource") return False # Usage resource = ReentrantResource() with resource as r: with resource as r2: # reentrant, no double acquire print(r, r2) # Output: Acquiring resource # resource_handler resource_handler # Releasing resource
with blocks and ensure the resource is released exactly once.contextlib Utility Functions Quick‑Ref
The contextlib module provides several utility context managers that handle common resource‑management patterns. Below is a reference table summarizing each function with its purpose and typical use case.
| Function | Description | Typical Use Case |
|---|---|---|
suppress(*exceptions) | Suppress specified exceptions within the block. | Ignoring FileNotFoundError when deleting a file that may not exist. |
redirect_stdout(new_target) | Redirect sys.stdout to a file‑like object. | Capturing print output in unit tests. |
redirect_stderr(new_target) | Redirect sys.stderr to a file‑like object. | Suppressing or capturing error output. |
nullcontext(enter_result=None) | A no‑op context manager; does nothing on entry/exit. | Conditional resource management: use it as a placeholder when no real resource is needed. |
closing(thing) | Calls on exit. | Wrapping objects that have a but no __enter__/__exit__. |
AbstractContextManager | Abstract base class for context managers. | Creating custom context managers that follow the protocol. |
contextmanager | Decorator to turn a generator into a context manager. | Simple setup/teardown without writing a class. |
asynccontextmanager | Decorator to turn an async generator into an async context manager. | Async resource management. |
ExitStack | Manages a dynamic stack of context managers. | Dynamic resource collections. |
Each of these utilities solves a specific problem and reduces boilerplate. For example, suppress is cleaner than a try/except with pass because it explicitly lists the exceptions you intend to ignore. redirect_stdout is invaluable for testing code that prints to stdout without modifying production code.
suppress can hide bugs. In production, always log suppressed exceptions at least at WARNING level.redirect_stdout is not thread‑safe; avoid it in concurrent production code.ExitStack is production‑ready and widely used in frameworks like pytest for fixture cleanup.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.Async Context Manager Snippet: __aenter__ and __aexit__
When you need a quick reference for writing an async context manager, the pattern is nearly identical to the synchronous version, but with coroutines. Below is a minimal snippet that demonstrates both the class‑based and decorator‑based approaches for managing an aiohttp session.
- Methods must be
async def. - Cleanup must be awaited.
- The exception suppression rule is the same: return
Falseto propagate,Trueto suppress.
Class-based async context managers are more explicit and allow state tracking. The @asynccontextmanager decorator is concise but has the same limitations as its synchronous counterpart: yield exactly once, and exceptions thrown into the generator must be handled with try/except.
import aiohttp from contextlib import asynccontextmanager # Class-based async context manager class AiohttpSessionManager: async def __aenter__(self): self._session = aiohttp.ClientSession() return self._session async def __aexit__(self, exc_type, exc_val, exc_tb): await self._session.close() return False # suppress? only if you must # Generator-based async context manager @asynccontextmanager async def managed_session(): session = aiohttp.ClientSession() try: yield session finally: await session.close() # Usage async def fetch(url: str): async with managed_session() as session: async with session.get(url) as resp: return await resp.json()
await inside __aexit__. If the close method is a coroutine but you don't await it, you'll get a warning or the coroutine will be garbage collected without running. Always use await for async cleanup.asyncio.wait_for() with a timeout for resource cleanup that might hang.event_loop fixture).Why You Need Context Managers: A Post-Mortem
Last month, I debugged a production pipeline where 50 concurrent workers silently burned through system file descriptors. The culprit? Devs manually calling close() on database cursors. One exception in the middle of the batch — boom, 800 open cursors. The system didn't crash immediately. It just got slower. Then slower. Then the OOM killer showed up.
Context managers exist because manual resource teardown is fragile. One unhandled exception, and your file handle, DB connection, or lock lives forever. The with statement guarantees cleanup — even if the code inside explodes. It's not about 'convenience'. It's about proving your resource lifecycle is correct under every failure path.
Think of __exit__ as your insurance policy. You write the setup, Python handles the teardown. No more try/finally blocks that someone 'forgets' to add. No more resource leaks that only surface in production at 3 AM. Your future self — and your on-call rotation — will thank you.
// io.thecodeforge # BAD: manual close, leak on exception def corrupt_csv_processor(): f = open('orders.lock', 'w') try: # Simulate a parsing crash raise ValueError('BOM encoding mismatch') finally: # Only runs if exception is caught locally pass # Forgot to close? Oops. f.close() # Never reached # GOOD: context manager guarantees cleanup def safe_csv_processor(): with open('orders.lock', 'w') as lock: raise ValueError('BOM encoding mismatch') # lock is closed, even after the exception print('Cleanup guaranteed.')
open() or connect() without a with block in production, you're gambling with resource exhaustion.Risks of Not Closing Resources: The File Descriptor Holocaust
Every open file, socket, or database connection consumes a file descriptor. Your OS — whether Linux, macOS, or Windows — has a hard limit. Default on most Linux: 1024 per process. Exceed that, and open() raises OSError with 'Too many open files'. Your app doesn't just slow down. It dies. No graceful shutdown. No log. Just a traceback that reaches your error tracker.
I've seen this in the wild: a batch job that processes 10,000 invoice PDFs. Each iteration opens a temp file, reads a signature, forgets to close. By iteration 800, the system says 'no more'. The whole job restarts from scratch because no checkpointing exists. That's a 3-hour job becoming a 6-hour nightmare.
Context managers are your shield. They eliminate the 'forgot to close' class of bugs. The with block is not optional for production code. Treat every open() as a liability. The only safe pattern is with open(...) as handle: inside a tight scope. No exceptions. No excuses.
// io.thecodeforge import os # Simulate a loop that doesn't close files def leak_fds(): handles = [] for i in range(2000): # BAD: no context manager f = open(f'/tmp/leak_{i}.tmp', 'w') handles.append(f) # f never closed if exception here print(f'Open FDs: {len(handles)}') if __name__ == '__main__': try: leak_fds() except OSError as e: print(f'CRASH: {e}') # This will print around FD 1024 on Linux
Database Connection Management with Context Manager
Your database connection pool has a max size — typically 10 to 50. Every unclosed connection blocks a slot. When all slots fill, new queries time out. Users see 500 errors. The DBA pings you. Fun times.
Using a context manager for database connections is not just best practice — it's survival. Here's the pattern: __enter__ gets a connection from the pool. __exit__ returns it, even on SQL errors or timeouts. Never leave connections in 'idle in transaction' state. That locks rows and kills concurrency.
Real talk: I've fixed production incidents where a single unclosed cursor held a row-level lock on an orders table. All subsequent writes queued up. 10 minutes of transaction backlogs. The fix was a one-line with block. Don't let your code be the reason the DBA sends you a late-night Slack. Wrap every query in a context manager. Your pool will thank you.
// io.thecodeforge import psycopg2 from contextlib import contextmanager @contextmanager def db_connection(conn_string): conn = psycopg2.connect(conn_string) try: yield conn conn.commit() # Only commit if no exception except Exception: conn.rollback() # Rollback on any error raise # Re-raise for caller to handle finally: conn.close() # Always return to pool # Usage - survivor pattern with db_connection('postgresql://user:pass@prod:5432/orders') as conn: with conn.cursor() as cur: cur.execute("UPDATE inventory SET qty = qty - 1 WHERE sku = 'ABC'") # If this UPDATE hangs or fails, rollback happens automatically # Connection is back in pool, no matter what
conn.rollback() in __exit__ if an exception occurred. Without it, the next query inherits a broken transaction state, causing 'current transaction is aborted' errors.The Silent Data Loss: When a Context Manager Swallowed the Exception
- Never return True from __exit__ for unknown exception types — it hides bugs.
- Always log suppressed exceptions at WARNING level.
- Test your context managers with exception injection (e.g., using monkeypatch).
throw(). Ensure your generator can handle being throw()n into. Use try/finally inside the generator.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)close() method and returns False (or None) to propagate exceptions.grep -rn 'def __exit__' src/Add temporary logging: '__exit__ called with exc_type=%s, exc_val=%s, exc_tb=%s'Trace the generator: import traceback; traceback.print_stack()Check for extra yields in the generator body after the main yield.| 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
Common mistakes to avoid
4 patternsReturning True from __exit__ for unknown exception types
Not wrapping generator-based context manager yield in try/finally
cleanup()Forgetting to make __aexit__ a coroutine in async context managers
Using depends_on without healthcheck in Docker Compose (analogous pattern)
Interview Questions on This Topic
What is a context manager in Python and why would you use one?
with statement to wrap a block of code, ensuring that resources are acquired before the block and released after the block, even if an exception occurs. Use them to manage file handles, network connections, locks, or any resource that requires deterministic cleanup.Explain how exception suppression works in context managers. When would you want to suppress an exception?
How does contextlib.contextmanager work under the hood? What are its limitations?
__next__() on the generator to run setup up to the yield. The yield value becomes the as target. When the with block exits, __next__() is called again to run teardown; if an exception occurred, it is thrown into the generator via throw(). Limitations: the generator must yield exactly once. If it yields twice, a RuntimeError occurs. Also, if the generator catches an exception and raises a different one, the original is lost unless chained.What is ExitStack and when should you use it instead of nested with statements?
enter_context(). When the ExitStack exits, all entered contexts are cleaned up in reverse order. Use it when you don't know at coding time how many resources you'll need (e.g., based on configuration), or when you need to conditionally enter contexts. Nested with statements are fine for fixed, known sets of resources.What happens if __exit__ itself raises an exception? Does the original exception get lost?
How would you create a context manager for a database transaction that commits on success and rolls back on failure?
self.conn.rollback() else: self.conn.commit() return FalseFrequently Asked Questions
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.
Yes, use the @contextmanager decorator from the contextlib module. It turns a generator function into a context manager, reducing boilerplate.
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.
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.
Use comma separation: with open('a') as a, open('b') as b:. Python 3.1+ allows this. For an unknown number, use ExitStack.
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.
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.
20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.
That's Exception Handling. Mark it forged?
12 min read · try the examples if you haven't