Python: Context Managers and with Blocks

Guide into Python's context management protocol, __enter__ and __exit__ mechanics, exception suppression, the contextlib module, and ExitStack.

Atul Jha
Atul Jha Systems & AI Researcher
20 min read
Python: Context Managers and with Blocks

Resource management is one of the most critical aspects of reliable software engineering. Sockets must be closed, locks must be released, database transactions must be committed or rolled back, and temporary files must be cleaned up—regardless of whether operations succeed or crash with unexpected exceptions.

Before Python 2.5, developers relied on sprawling, error-prone try...finally blocks. The introduction of PEP 343 and the with statement introduced Context Managers, creating a standardized, declarative protocol for deterministic setup and teardown.

In this comprehensive article, following Part 2 Section 10 of Fred Baptiste’s Python Series, we dissect the mechanics of __enter__ and __exit__, understand exception propagation, master @contextlib.contextmanager, and leverage ExitStack for dynamic resource orchestration.


1. Anatomy of the Context Management Protocol

A context manager is any Python object that implements the Context Management Protocol via two dunder methods:

class ContextManagerProtocol:
    def __enter__(self):
        """Executed before the with-block starts.
        Whatever this returns is bound to the variable after 'as'.
        """
        ...

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Executed after the with-block terminates (or crashes).
        Receives exception information if an error occurred.
        """
        ...

The Bytecode Behind the with Statement

When Python encounters a with statement:

with ContextManager() as resource:
    # do something with resource
    ...

It is translated by CPython into this equivalent robust execution scaffold:

manager = ContextManager()
enter_res = manager.__enter__()
target = enter_res  # bound to 'as target'

exc = True
try:
    try:
        # with-block body executes here
        pass
    except:
        exc = False
        # If __exit__ returns True, exception is swallowed
        if not manager.__exit__(*sys.exc_info()):
            raise
finally:
    if exc:
        manager.__exit__(None, None, None)

2. Building a Custom Context Manager from Scratch

Let’s build a practical database transaction manager that commits on success and rolls back on failure:

class DatabaseConnection:
    def execute(self, query: str):
        print(f"  [DB] Executed: {query}")

class Transaction:
    def __init__(self, conn: DatabaseConnection):
        self.conn = conn

    def __enter__(self):
        print("BEGIN TRANSACTION")
        self.conn.execute("SET TRANSACTION READ WRITE")
        return self.conn  # Binds to the 'as' target

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            print(f"ROLLBACK TRANSACTION due to {exc_type.__name__}: {exc_val}")
            # Returning False (or None) allows the exception to bubble up
            return False
        else:
            print("COMMIT TRANSACTION")
            return True

db = DatabaseConnection()

# Successful transaction
print("--- Case 1: Success ---")
with Transaction(db) as conn:
    conn.execute("INSERT INTO users (name) VALUES ('Alice')")

# Failing transaction
print("\n--- Case 2: Failure ---")
try:
    with Transaction(db) as conn:
        conn.execute("INSERT INTO users (name) VALUES ('Bob')")
        raise ValueError("Invalid balance detected")
except ValueError:
    print("Caught exception in outer scope!")

Output:

--- Case 1: Success ---
BEGIN TRANSACTION
  [DB] Executed: SET TRANSACTION READ WRITE
  [DB] Executed: INSERT INTO users (name) VALUES ('Alice')
COMMIT TRANSACTION

--- Case 2: Failure ---
BEGIN TRANSACTION
  [DB] Executed: SET TRANSACTION READ WRITE
  [DB] Executed: INSERT INTO users (name) VALUES ('Bob')
ROLLBACK TRANSACTION due to ValueError: Invalid balance detected
Caught exception in outer scope!

3. Exception Suppression and Control Flow

The __exit__ method receives four parameters:

  • self: The context manager instance
  • exc_type: The exception class (e.g., ZeroDivisionError) or None
  • exc_val: The exception instance or None
  • exc_tb: The traceback object or None

Suppressing Specific Exceptions

If __exit__ returns a truthy value (True), CPython suppresses the exception:

class SuppressErrors:
    def __init__(self, *exceptions_to_ignore):
        self._exceptions = exceptions_to_ignore

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None and issubclass(exc_type, self._exceptions):
            print(f"Suppressed expected exception: {exc_val}")
            return True  # Suppress!
        return False  # Propagate all other unexpected exceptions

with SuppressErrors(FileNotFoundError, KeyError):
    data = {}
    print(data["missing_key"])  # Suppressed!

print("Program safely continues...")

[!WARNING] Never return True indiscriminately from __exit__. Doing so silences all errors including KeyboardInterrupt, MemoryError, and typo-induced NameError, turning debugging into a nightmare.


4. Generator-Based Context Managers: @contextlib.contextmanager

Writing an entire class with __enter__ and __exit__ for simple setup/teardown tasks can feel verbose. The standard library provides the @contextlib.contextmanager decorator, turning a Python generator into a full context manager:

from contextlib import contextmanager
import time

@contextmanager
def timed_block(label: str):
    start = time.perf_counter()
    print(f"[{label}] starting...")
    try:
        yield  # Control transfers to the with-block
    finally:
        # Always executes on exit or exception
        duration = time.perf_counter() - start
        print(f"[{label}] completed in {duration * 1000:.2f} ms")

with timed_block("Compute heavy task"):
    sum(x * x for x in range(1_000_000))

Protocol Mapping:

  1. Everything before the yield statement corresponds to __enter__().
  2. The value provided to yield <val> becomes the as <target> binding.
  3. Execution inside the with block runs while the generator is suspended at yield.
  4. Everything after yield (or inside a finally block) executes as __exit__().
  5. If an exception occurred inside the with block, it is re-raised at the point of yield inside your generator, allowing you to catch it with except.
@contextmanager
def managed_resource():
    print("1. Allocating resource")
    res = {"state": "active"}
    try:
        yield res
    except Exception as e:
        print(f"2. Handling error: {e}")
        # Re-raise unless intentionally suppressed
        raise
    finally:
        print("3. Teardown: Releasing resource")

5. Standard Library Helpers in contextlib

Python provides several indispensable context manager utilities:

1. contextlib.suppress

A clean one-liner replacement for verbose try...except...pass blocks:

from contextlib import suppress
import os

# Cleanly remove file without crashing if it doesn't exist
with suppress(FileNotFoundError):
    os.remove("temporary_cache.tmp")

2. contextlib.redirect_stdout / redirect_stderr

Temporarily intercepts and redirects standard stream output to an in-memory buffer or file:

from contextlib import redirect_stdout
import io

buffer = io.StringIO()
with redirect_stdout(buffer):
    print("This goes into the buffer, not your terminal screen!")
    print("Second line of output.")

captured = buffer.getvalue()
print("Captured text:", repr(captured))

3. contextlib.closing

For objects (like database connections or network sockets) that define a .close() method but do not implement __enter__ and __exit__:

from contextlib import closing
from urllib.request import urlopen

with closing(urlopen("https://httpbin.org/get")) as page:
    for line in page:
        pass  # page.close() is guaranteed to be called

6. Dynamic Orchestration with contextlib.ExitStack

What if you need to open a dynamic list of files whose names are only known at runtime?

# Impossible with standard static syntax:
# with open(f1), open(f2), open(f3)... ? How many files?

contextlib.ExitStack maintains a dynamic LIFO (Last-In, First-Out) stack of context managers and cleanup callbacks:

from contextlib import ExitStack

filenames = ["file_a.txt", "file_b.txt", "file_c.txt"]

# Dynamically open and manage an arbitrary number of files
with ExitStack() as stack:
    # Every file entered via enter_context is pushed onto the stack
    files = [
        stack.enter_context(open(name, "w"))
        for name in filenames
    ]

    for idx, f in enumerate(files):
        f.write(f"Sample data for worker {idx}\n")

# Once the with-block terminates, ExitStack closes all files in reverse order!

Registering Arbitrary Teardown Callbacks

ExitStack also allows registering generic callback functions that are guaranteed to run upon exiting:

with ExitStack() as stack:
    stack.callback(print, "Cleanup step 1 (executed second)")
    stack.callback(print, "Cleanup step 2 (executed first)")
    print("Doing work inside stack...")

7. Reentrant vs. Single-Use Context Managers

Most context managers are single-use: once exited, entering them a second time raises an error (for example, reading a closed file).

However, some context managers are reentrant and can be entered multiple times, even recursively:

  • threading.RLock (reentrant lock)
  • Custom state-tracking managers
import threading

rlock = threading.RLock()

# Reentrant lock can be acquired multiple times by the same thread
with rlock:
    print("Lock acquired layer 1")
    with rlock:
        print("Lock acquired layer 2 (reentrant)")

Summary Checklist for Writing Robust Context Managers:

  1. Ensure __enter__ returns the resource the caller actually needs in as target.
  2. Clean up resources inside __exit__ regardless of whether exc_type is None.
  3. Return True from __exit__ only if you deliberately want to silence the error.
  4. Prefer @contextlib.contextmanager for concise generator-based cleanup.
  5. Use ExitStack whenever handling dynamic arrays of context managers.

Interactive Code Lab: Python: Context Managers and with Blocks

Python SystemsMatched to lesson

Implements a scoped resource manager guaranteeing cleanup on exceptions.

Labs:
Custom Context Manager with __enter__ and __exit__
Python 3.13 • NumPy • PyTorch
Terminal Output

Click Run Code to execute this algorithm in the browser sandbox.

How did you find this blueprint?

Tap a reaction to share instant feedback with the engineering team.

Frequently Asked Questions

Frequently Asked Questions

General

Citations & Recommended References

References

No image

Fred Baptiste's masterclass on Context Managers, __enter__, __exit__, and contextlib.

No image

The Python Enhancement Proposal defining the with statement and context management protocol.

Atul Jha
Written by
Atul Jha

AI Researcher and Systems Engineer focusing on production machine learning pipelines, transformer architectures, and performant Python runtime internals.

View Profile →
Share Blueprint:

Discussion & Community Thoughts

0%
Notification