Mid-level 12 min · March 05, 2026
Context Managers in Python

Python __exit__ Returning True — The Silent Bug Pattern

A single return True in __exit__ caused 5% data loss by swallowing IntegrityError silently.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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
✦ Definition~90s read
What is Context Managers in Python?

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.

Imagine you borrow a library book.

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.

Plain-English First

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.

io/thecodeforge/context_manager_basic.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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')
Output
(file written)
Core Insight
The real value isn't syntax sugar — it's guarantee. __exit__ is called even if you return early, raise, or hit a system exit. Only a process kill or hardware failure skips it.
Production Insight
In production, the most common failure is a context manager that doesn't actually close the resource.
Check if __exit__ calls the resource's close() or release() — not just returns True.
Test with strace to confirm file descriptor counts stay flat.
Rule: always return False unless you explicitly intend to suppress.
Key Takeaway
Context managers encode the cleanup contract at the resource, not the caller.
__exit__ is always called for normal and exceptional exits.
The safe default for __exit__ is return False.
Python __exit__ Returning True — The Silent Bug Pattern THECODEFORGE.IO Python __exit__ Returning True — The Silent Bug Pattern Flow of context manager execution and exception suppression Context Manager Entry __enter__ returns resource, no exception yet Code Block Execution User code runs; may raise exception __exit__ Called Receives exception type, value, traceback Return True in __exit__ Suppresses exception, continues silently Exception Propagates Default False or no exception: normal flow ⚠ Returning True in __exit__ swallows exceptions silently Only return True if you intentionally suppress and handle the error THECODEFORGE.IO
thecodeforge.io
Python __exit__ Returning True — The Silent Bug Pattern
Context Managers Python

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.

io/thecodeforge/contextmanager_enter_exit.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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')
Output
(file written)
Parameter Trap
The __exit__ signature is strict: three positional args (exc_type, exc_val, exc_tb). Omit one and you get a TypeError at runtime, not at definition. Always use *args or proper parameter names.
Production Insight
The __exit__ method's signature is strict: three positional arguments.
If you accidentally omit one, Python raises a TypeError at runtime — not at definition time.
In production, always use *args or match the exact signature to avoid surprising exceptions.
Log the exception details inside __exit__ before deciding to suppress.
Key Takeaway
__enter__ is the setup hook; __exit__ is the teardown hook.
Both are always called, even when the block raises an exception.
The safe default is return False — let exceptions propagate.

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.

io/thecodeforge/exception_context.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
Output
(no output on success; warning logged on error)
Exception Transformation Gotcha
If you raise a new exception inside __exit__, Python treats that as the exception to propagate. The original exception is lost unless you chain it using raise ... from. Always log the original or chain it to avoid silent data loss.
Production Insight
Raising inside __exit__ while an exception is already active is tricky.
CPython 3.7+ will set the new exception's __context__ to the old one automatically.
But if __exit__ raises after a successful block, that new exception bubbles out alone.
Test both paths: block succeeds, block fails.
Never leave an unguarded cleanup in __exit__ — wrap risky operations in try/except.
Key Takeaway
Use __exit__ to log, transform, or suppress exceptions.
Return True to swallow an exception; return False to propagate.
When chaining, use 'raise NewError from original' to preserve context.

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.

io/thecodeforge/when_to_suppress.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
Output
(no output; log message on OSError)
Suppression Audit
If you return True anywhere in __exit__, you must log the exception. Without logging, you are blind to failures. In production, add a structured log with the exception type and message.
Production Insight
In production, never default to returning True. Instead, use a whitelist of exception types you are willing to suppress. Log every suppressed exception with its full traceback at WARNING level. Monitor the rate of suppressed exceptions—an increase often signals an underlying problem.
If you inherit legacy code that unconditionally returns True, first add logging of the exception details, then change to return False for unknown types. Test by injecting exceptions to verify the new behavior.
Key Takeaway
Only return True from __exit__ for exceptions you understand and expect.
Always log at WARNING before suppression.
When in doubt, return False and let the exception propagate.
Decision Flow for Returning True in __exit__
Yes, known safeNo, unknownPartially handledException occurs in with blockDoes __exit__ handle it?Log at WARNING, return TrueLog at ERROR, return FalseLog, may chain or suppress onlyspecific typesSuppress exceptionLet exception propagateDepends on logic

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.

io/thecodeforge/contextlib_example.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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')
Output
(file written)
Mental Model: Generator as Two Halves
  • 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.
Production Insight
Generator-based context managers are tempting but have a critical caveat: if the generator yields twice, it raises StopIteration and the with statement sees that as an exception.
Always yield exactly once.
Also, if the managed block raises an exception that the generator handles but then raises a different exception, the second exception propagates and the first is lost.
Wrap yield in try/finally, and use try/except inside to handle and chain exceptions properly.
Key Takeaway
contextlib.contextmanager reduces boilerplate but requires discipline.
Yield exactly once, wrap in try/finally, and never suppress exceptions unintentionally.
Test with exception injection to verify correct behavior.

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.

Other advanced tools from contextlib
  • suppress(): temporarily ignore specific exceptions.
  • redirect_stdout/stderr: redirect streams (useful in tests).
  • nullcontext(): a no-op context manager for conditional resource handling.
  • closing(): wraps a closeable object.

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.

io/thecodeforge/exitstack_example.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
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.
Output
Opening db0
Opening db1
Opening db2
All connections open: ['conn_db0', 'conn_db1', 'conn_db2']
Closing db2
Closing db1
Closing db0
Production Insight
ExitStack is invaluable when the set of resources is dynamic (e.g., based on configuration).
But misuse can lead to resources being held longer than expected if you push too many items.
Another pitfall: if one of the enter_context calls raises, all previously entered contexts are still cleaned up properly — that's the main benefit.
The cleanup always happens, but monitoring of partial failures is your responsibility.
Log how many contexts you entered to facilitate debugging.
Key Takeaway
Use ExitStack for dynamic resource management.
Cleanup order is always reverse of entry — ExitStack does that automatically.
For static contexts, prefer comma-separated with statements over nesting for readability.
When to Use Which Context Manager Pattern
IfSingle resource, simple setup/teardown
UseUse contextlib.contextmanager generator
IfMultiple resources, fixed at write time
UseUse multiple with commas: with A as a, B as b:
IfDynamic number of resources or conditional inclusion
UseUse ExitStack and push via enter_context
IfYou need to suppress a specific exception in a small section
UseUse contextlib.suppress(ExpectedException)

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.

io/thecodeforge/exitstack_multiple.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
Output
(connects to each db, runs migration, then closes connections in reverse order)
Partial Initialization
If a later connection fails, all previously opened connections are still properly closed because EnterStack triggers cleanup for all entered contexts. This is the key safety guarantee.
Production Insight
ExitStack is not just for dynamic counts — it also handles conditional resources. Use stack.enter_context() inside an if block to only open a resource when needed.
Be careful with stack.push() vs stack.enter_context(): push() adds an already‑entered context manager to the cleanup stack, while enter_context() both enters and pushes.
For resources that support enter, always use enter_context() to ensure proper initialization.
Key Takeaway
ExitStack is the safe way to manage a dynamic or unknown number of resources.
It guarantees cleanup in reverse order, even on partial entry failure.
Use enter_context() to both open and register for cleanup.
ExitStack Cleanup Flow
Start ExitStackPush context 1Push context 2Push context 3 - failsCleanup context 2Cleanup context 1Raise exception from context 3

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@contextmanagerExitStack
PurposeTurn a generator into a single context managerManage a dynamic stack of multiple context managers
Setup/TeardownCode before yield = setup; code after yield = teardownPush contexts via enter_context(), cleanup is automatic LIFO
Number of ResourcesExactly one resource per generatorUnlimited; dynamic at runtime
Exception ControlException thrown into generator at yield; you handle with try/exceptEach individual context manager handles its own exceptions; overall suppression controlled by ExitStack's __exit__
LimitationsMust yield exactly once; tricky exception chainingSlightly more verbose; must be careful with push() vs enter_context()
Use CaseSingle resource with simple setup/teardownMultiple 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.

io/thecodeforge/quickref_cm_exitstack.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
Output
Acquire file1
Using file1
Release file1
Acquire file0
Acquire file1
Acquire file2
Using ['file0', 'file1', 'file2']
Release file2
Release file1
Release file0
When in Doubt, Start Simple
Begin with a class-based manager if logic is complex. Use @contextmanager for quick wrappers. Only reach for ExitStack when you truly have dynamic resources.
Production Insight
In production, prefer ExitStack over manual nesting of with statements when dealing with more than two resources. ExitStack's LIFO behavior is predictable and the pop_all() method can move contexts into a broader scope when needed.
Beware of using ExitStack with very large numbers of contexts – each context manager adds overhead. Profile if you have hundreds.
For @contextmanager, the main gotcha is that if the generator is garbage-collected before the with block finishes, it may raise RuntimeError. Ensure the generator stays alive by keeping a reference (the with statement does this automatically).
Key Takeaway
@contextmanager is ideal for single, simple resources; ExitStack handles any number of dynamic contexts.
Refer to the table to quickly choose the right tool for your use case.

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.

io/thecodeforge/reentrant_cm.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
Output
Acquiring resource
resource_handler resource_handler
Releasing resource
Reentrancy != Thread Safety
Reentrancy in this context means the same object can be used as a context manager multiple times, potentially nested. It does not automatically make it thread‑safe – you still need locks if used from multiple threads.
Production Insight
Reentrant context managers are not common in production because most resources (files, sockets, database connections) are not designed for nested use.
If you need reentrancy, implement a counter pattern as shown. Be careful to decrement on every __exit__, and only release on zero.
Testing is critical: simulate nested with blocks and ensure the resource is released exactly once.
Key Takeaway
Reentrant context managers can be entered multiple times, even nested.
Implement with an enter count to avoid double‑acquire or double‑release.
Most real‑world resources are non‑reentrant; only use reentrant patterns when necessary.

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.

FunctionDescriptionTypical 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 thing.close() on exit.Wrapping objects that have a close() but no __enter__/__exit__.
AbstractContextManagerAbstract base class for context managers.Creating custom context managers that follow the protocol.
contextmanagerDecorator to turn a generator into a context manager.Simple setup/teardown without writing a class.
asynccontextmanagerDecorator to turn an async generator into an async context manager.Async resource management.
ExitStackManages 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.

Prefer contextlib over custom classes
For many common patterns, contextlib utilities eliminate the need to write your own context manager class. Use them to keep your codebase small and testable.
Production Insight
Overusing 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.
Key Takeaway
contextlib provides ready‑made context managers for common patterns.
Refer to the table when you need to decide which utility to use.
Always log suppressed exceptions to avoid silent failures.
contextlib Utilities Relationship
contextlibcontextmanagerasynccontextmanagersuppressredirect_stdoutredirect_stderrnullcontextclosingExitStackGenerator-based CMAsync generator CMDynamic nesting

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.

io/thecodeforge/test_context_manager.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
Output
(no test output on success)
Test Every Path
Use pytest's monkeypatch or unittest.mock to make __exit__ raise, make the managed block raise, and ensure cleanup still happens. Don't rely on coverage numbers — test each exception scenario explicitly.
Production Insight
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.
Always log both when developing the context manager.
Your test suite should include a test where both the block and __exit__ raise.
Key Takeaway
Test context managers with exception injection for all three phases.
Happy-path testing only catches half the bugs.
Use pytest fixtures to simulate resource failures and verify cleanup.

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.

io/thecodeforge/async_context_manager.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
Output
(database query executed; connection closed)
Async Gotcha
If you forget the 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.
Production Insight
Async context managers hide the same pitfalls as sync ones, plus one more: if __aexit__ is not truly async (doesn't await anything), the event loop may block.
Use asynccontextmanager for quick patterns, but class-based managers when you need complex cleanup logic.
Always test with exception injection in both sync and async paths.
Key Takeaway
Async context managers use __aenter__/__aexit__ coroutines.
Use asynccontextmanager for simple cases.
The same exception suppression rules apply — return False to propagate.

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.

Note the critical differences
  • Methods must be async def.
  • Cleanup must be awaited.
  • The exception suppression rule is the same: return False to propagate, True to 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.

io/thecodeforge/async_snippet.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()
Output
(async HTTP request result)
Don't forget await
A common bug is forgetting 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.
Production Insight
When using async context managers in production, ensure your cleanup does not introduce long blocking operations. Use asyncio.wait_for() with a timeout for resource cleanup that might hang.
If your service uses connection pools, the context manager should return the connection to the pool rather than closing it entirely — adjust the __aexit__ accordingly.
Always test async context managers with an event loop that runs at the end to catch unclosed resources (use pytest‑asyncio's event_loop fixture).
Key Takeaway
Async context managers follow the same pattern as sync but with coroutines.
Always await cleanup and return False to propagate exceptions.
Use class-based for complex state, decorator for simple resources.

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.

resource_leak_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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.')
Output
Traceback (most recent call last):
File "resource_leak_demo.py", line 8, in corrupt_csv_processor
raise ValueError('BOM encoding mismatch')
ValueError: BOM encoding mismatch
# No output from bad version - file stays open
# Good version prints nothing, but file is closed
Production Trap:
Never rely on the OS to clean up file descriptors after your process exits. Long-running services accumulate leaked handles until you hit ulimit -n. That's a hard crash with zero graceful recovery.
Key Takeaway
If you write 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.

fd_leak_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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
Output
CRASH: [Errno 24] Too many open files: '/tmp/leak_1024.tmp'
# Process must exit or be killed. All work lost.
Production Trap:
ulimit -n shows your per-process limit. Monitor /proc/<pid>/fd in production dashboards. A rising FD count is a five-alarm fire.
Key Takeaway
One unclosed file descriptor per iteration in a loop is a ticking time bomb. with blocks are mandatory, not optional.

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.

db_conn_mgr.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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
Output
# On successful execution: connection committed, then closed
# On exception: connection rolled back, then closed, exception re-raised
# Example output for success: (no output, silent success)
# Example output for exception:
# Traceback (most recent call last):
# File "db_conn_mgr.py", line 20, in <module>
# raise psycopg2.DatabaseError('Deadlock detected')
# psycopg2.DatabaseError: Deadlock detected
Production Trap:
Always call conn.rollback() in __exit__ if an exception occurred. Without it, the next query inherits a broken transaction state, causing 'current transaction is aborted' errors.
Key Takeaway
A context manager for DB connections is a commit/rollback guarantee. Never let a connection escape without being returned to the pool.
● Production incidentPOST-MORTEMseverity: high

The Silent Data Loss: When a Context Manager Swallowed the Exception

Symptom
Records were partially written; no errors in logs. Monthly reconciliation reports showed discrepancies of up to 5%.
Assumption
The DB connection context manager handled exceptions correctly—it was tested with unit tests that only verified the happy path.
Root cause
The __exit__ method returned True unconditionally, suppressing every exception including IntegrityError and DataError. The transaction was never rolled back.
Fix
Return False from __exit__ unless the exception type is explicitly handled. Add logging before suppression. Use contextlib.suppress only for expected exceptions.
Key lesson
  • 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).
Production debug guideDiagnose the most common context manager failures in production4 entries
Symptom · 01
Resource (file, socket) not closed after with block
Fix
Verify __exit__ is being called: add a print or log statement. Check for early returns inside the managed block that skip the with statement? No – with always calls __exit__. Likely the context manager object is not properly implementing the protocol.
Symptom · 02
Exception silently swallowed – no traceback for expected errors
Fix
Inspect __exit__ return value. If it returns True, exceptions are suppressed. Return False (or None) to propagate. Use a logging decorator around __exit__.
Symptom · 03
Nested context managers: outer cleanup fails, inner never called
Fix
Use contextlib.ExitStack to manage dynamic nesting. Avoid manual nesting with try/finally around multiple with statements. Check CPython 3.7+ guaranteed cleanup order: inner first, then outer.
Symptom · 04
Generator-based (contextlib.contextmanager) context manager yields twice
Fix
The generator must yield exactly once. If the managed block raises, the generator will receive that exception via throw(). Ensure your generator can handle being throw()n into. Use try/finally inside the generator.
★ Quick Debug Cheat Sheet: Context ManagersThe three most common context manager failures and the exact commands to diagnose them.
Resource not released after with block
Immediate action
Check if __exit__ is defined correctly and called.
Commands
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)
Fix now
Ensure __exit__ invokes the resource's close() method and returns False (or None) to propagate exceptions.
Exception suppressed – no traceback+
Immediate action
Find the __exit__ method and check the return value.
Commands
grep -rn 'def __exit__' src/
Add temporary logging: '__exit__ called with exc_type=%s, exc_val=%s, exc_tb=%s'
Fix now
Return False if the exception is unexpected. Use contextlib.suppress only for known, safe exceptions.
Generator-based context manager resumes after yield – unexpected value+
Immediate action
Ensure the generator yields exactly once.
Commands
Trace the generator: import traceback; traceback.print_stack()
Check for extra yields in the generator body after the main yield.
Fix now
Wrap the yield in try/finally and avoid extra yields.
Context Manager Creation Methods
ApproachBoilerplateException ControlUse Case
Class with __enter__/__exit__More verbose – full classFull control – inspect, suppress, transformComplex resources needing custom state
Generator with @contextmanagerMinimal – single functionLimited – exception arrives at yield, you can handle in try/finallySimple setup/teardown, single resource
contextlib.suppress()One-line wrapperSuppresses specific exception typesIgnoring expected errors (e.g., FileNotFoundError when deleting)
ExitStackDynamic push/popStack-level cleanup; individual manager exceptions propagateManaging groups of dynamic resources
Async class with __aenter__/__aexit__More verbose – full async classFull async controlAsync resources (DB, HTTP sessions)
Async generator with @asynccontextmanagerMinimal – single async functionLimited – same as sync generatorSimple async setup/teardown

Key takeaways

1
Context managers encode the cleanup contract at the resource, not the caller.
2
__enter__ and __exit__ give you full control over resource lifecycle and exception handling.
3
contextlib.contextmanager reduces boilerplate but requires strict one-yield and try/finally.
4
ExitStack handles dynamic resource collections; cleanup order is always LIFO.
5
Test context managers with exception injection
happy-path testing is not enough.
6
Returning True from __exit__ suppresses exceptions; most of the time you want False.
7
Async context managers use __aenter__/__aexit__; same rules apply for suppression.
8
Log any suppressed exceptions at WARNING level to avoid silent data loss.

Common mistakes to avoid

4 patterns
×

Returning True from __exit__ for unknown exception types

Symptom
Exceptions are swallowed silently; no traceback; hard to debug data corruption.
Fix
Only return True when you have explicitly handled the exception. Otherwise return False or None. Log all suppressed exceptions.
×

Not wrapping generator-based context manager yield in try/finally

Symptom
If the managed block raises an exception, the cleanup code after yield never runs.
Fix
Always use try/finally around the yield to guarantee teardown. Example: try: yield finally: cleanup()
×

Forgetting to make __aexit__ a coroutine in async context managers

Symptom
RuntimeError: 'async with' requires async __aexit__ method
Fix
Use async def __aexit__(self, ...) and await any cleanup calls inside it.
×

Using depends_on without healthcheck in Docker Compose (analogous pattern)

Symptom
Service starts before dependency is ready, causing connection failures.
Fix
Use healthcheck and condition: service_healthy to ensure readiness.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is a context manager in Python and why would you use one?
Q02SENIOR
Explain how exception suppression works in context managers. When would ...
Q03SENIOR
How does contextlib.contextmanager work under the hood? What are its lim...
Q04SENIOR
What is ExitStack and when should you use it instead of nested with stat...
Q05SENIOR
What happens if __exit__ itself raises an exception? Does the original e...
Q06SENIOR
How would you create a context manager for a database transaction that c...
Q01 of 06JUNIOR

What is a context manager in Python and why would you use one?

ANSWER
A context manager is an object that defines __enter__ and __exit__ methods. It is used with the 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.
FAQ · 7 QUESTIONS

Frequently Asked Questions

01
What is a context manager in Python in simple terms?
02
Can I use a context manager without a class?
03
What happens if I return True from __exit__ without handling the exception?
04
What is the difference between contextlib.suppress and a try/except?
05
How do I use multiple context managers in one with statement?
06
What should I do if my context manager fails to clean up when an exception occurs?
07
How do async context managers differ from sync ones?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Exception Handling. Mark it forged?

12 min read · try the examples if you haven't

Previous
raise and assert in Python
5 / 5 · Exception Handling
Next
File Handling in Python