🐍 Python Course · Stage 1 · Lesson 15 of 89 · Context Managers
Course Overview →

🟩 Start with the Beginner tab. Read it fully before moving on.

← Generators Object-Oriented Python →
thecodex.expert · The Codex Family of Knowledge
Python

Context Managers in Python

A context manager is an object that sets up and tears down a resource automatically using the with statement — guaranteeing cleanup happens even if an exception occurs.

Python 3.13 docs.python.org Last verified:
Canonical Definition

A context manager is an object that defines __enter__ and __exit__ methods. When used in a with statement, Python calls __enter__ at the start (setup) and __exit__ at the end (teardown, even if an exception was raised). The with statement is defined in the Python Language Reference as the primary mechanism for resource management.

🟩 Beginner

Guaranteed setup and teardown with with

What you'll learn: What the with statement does, why it guarantees cleanup even on exceptions, and which built-in objects support it. After this, you'll never open a file without with again.

How to read this tab: Deliberately cause an exception inside a with open() block and observe that the file is still properly closed. Then try the same without with and see the file handle left open.

⏱ 30 min📄 2 sections🔶 Prerequisite: Error Handling
The idea

A context manager is like a responsible adult who not only opens a door for you but guarantees they'll close it when you're done — even if you trip and fall while going through. The with statement is the door; __exit__ is the closing.

with is not just a style preference — it's the only safe way to manage resources. try/finally is equivalent but verbose. The with statement makes the contract explicit: this block uses this resource, and the resource is always cleaned up when the block exits. Files, locks, database connections, network sockets — all should be managed with with.

The with statement

The most common context manager in Python is file handling. Without with, you must remember to call f.close() — and if an exception occurs before that line, the file stays open. With with, the file is always closed, regardless of what happens.

Pythonwith_statement.py
# WITHOUT with — dangerous: close() may not run on exception
f = open("data.txt", "r")
try:
    content = f.read()
finally:
    f.close()   # must remember this

# WITH with — always closes, even on exception
with open("data.txt", encoding="utf-8") as f:
    content = f.read()
# f.close() is called automatically here — always

# Multiple context managers in one with (Python 3.1+)
with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

# Parenthesised form for many managers (Python 3.10+)
with (
    open("a.txt") as a,
    open("b.txt") as b,
    open("c.txt") as c,
):
    data = a.read() + b.read() + c.read()
💡

The most important context manager you're not using: unittest.mock.patch(). Use it as a context manager (not just a decorator) when you need mock to be active for only part of a test: with patch("module.func") as mock_func:. The mock is automatically removed when the with block exits, even on test failure.

Common built-in context managers

Many Python built-ins and standard library modules support the context manager protocol:

Context ManagerManagesOn exit
open()File handlesCloses the file
threading.Lock()Thread locksReleases the lock
decimal.localcontext()Decimal precisionRestores old precision
unittest.mock.patch()Mocked objectsRestores original
contextlib.suppress()Expected exceptionsSilences named exceptions
Database connectionsDB connectionsCommits or rolls back

✅ Beginner tab complete

  • I always use "with open(...) as f:" — never open() without with
  • I know with guarantees __exit__ runs even if an exception occurs inside the block
  • I can list 4 common Python objects that support the context manager protocol
  • I can use multiple context managers in a single with statement

Continue to Object-Oriented Python →

🔵 Intermediate

Implementing __enter__ and __exit__, and @contextmanager

What you'll learn: How to implement the full context manager protocol (__enter__ and __exit__). How to write context managers as generator functions using @contextlib.contextmanager. contextlib.suppress for silently ignoring specific exceptions.

How to read this tab: Write a Timer context manager class from scratch (enter records start time, exit prints elapsed). Then rewrite it as a @contextmanager generator. Compare the two approaches — both work; choose based on complexity.

⏱ 35 min📄 2 sections🔶 Prerequisite: Beginner tab
💡

Returning True from __exit__ suppresses the exception — use with care. If your __exit__ always returns True, it silently swallows ALL exceptions. This hides bugs. Only return True when you specifically intend to suppress the exception (like contextlib.suppress does for named exception types). For most context managers, return False or None — let exceptions propagate.

The __enter__ and __exit__ protocol

Any object implementing __enter__ and __exit__ can be used in a with statement. __enter__ is called at the start and its return value is bound to the as variable. __exit__ is called at the end, receiving exception information if one occurred — it can suppress the exception by returning a truthy value.

Pythoncontext_protocol.py
class Timer:
    """Context manager that measures elapsed time."""
    import time

    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self   # bound to the 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.perf_counter() - self.start
        print(f"Elapsed: {self.elapsed:.4f}s")
        return False  # False = don't suppress exceptions

with Timer() as t:
    total = sum(range(10_000_000))
# Elapsed: 0.312s

print(t.elapsed)   # also accessible after the with block

# __exit__ signature: exc_type, exc_val, exc_tb
# If no exception: all three are None
# If exception: they contain the exception info
# Return True to suppress the exception; False/None to propagate it

class SuppressIOError:
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        return exc_type is IOError   # suppress IOErrors only

@contextmanager is the shortest path to a working context manager. Code before yield = __enter__. yield value = the value bound to the as variable. Code in finally after yield = __exit__ cleanup. Wrap the yield in try/finally to ensure cleanup always runs. This is the pattern used by the Python standard library itself for most of its context managers.

Writing context managers with contextlib

The contextlib module's @contextmanager decorator lets you write a context manager as a generator function — simpler than implementing the full class protocol. Everything before yield is the __enter__ body; everything after is the __exit__ body.

Pythoncontextmanager_decorator.py
from contextlib import contextmanager
import os, tempfile

@contextmanager
def temporary_directory():
    """Create a temp dir, yield it, delete it on exit."""
    tmpdir = tempfile.mkdtemp()
    try:
        yield tmpdir        # __enter__ returns this value
    finally:
        import shutil
        shutil.rmtree(tmpdir)   # __exit__ cleanup

with temporary_directory() as tmpdir:
    # work inside the temp directory
    filepath = os.path.join(tmpdir, "data.txt")
    with open(filepath, "w") as f:
        f.write("temporary data")
# tmpdir is deleted here, regardless of exceptions

# contextlib.suppress — the cleanest way to ignore specific exceptions
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("might_not_exist.txt")
# No try/except needed — FileNotFoundError is silently ignored
Commonly confused
The as variable is NOT the context manager itself — it's what __enter__ returns. For open(), __enter__ returns the file object, not the open() return value. For threading.Lock(), __enter__ returns the lock. For your own context managers, return whatever the user needs to work with inside the block.
__exit__ receives exception info — use it, don't ignore it. If your __exit__ always returns True, it silently swallows ALL exceptions raised inside the with block. Only return True if you intentionally want to suppress the exception. Returning False or None propagates exceptions normally.

✅ Intermediate tab complete

  • I can implement __enter__ and __exit__ on a class to make it a context manager
  • I know __exit__ receives (exc_type, exc_val, exc_tb) — all None if no exception
  • I can write a context manager using @contextmanager with yield as the boundary
  • I know contextlib.suppress(FileNotFoundError) as an alternative to try/except/pass

Continue to Object-Oriented Python →

🔴 Expert

ExitStack, asynccontextmanager, and the with desugar

What you'll learn: contextlib.ExitStack for managing a dynamic number of context managers. asynccontextmanager and async with for async context management. The precise desugar of the with statement per the Python Language Reference.

How to read this tab: Build a function that opens N files from a list (unknown at write time) using ExitStack. This pattern appears in test fixtures, batch processors, and connection pools.

⏱ 20 min📄 1 section🔶 Prerequisite: After Stage 2

contextlib advanced tools

The contextlib module provides several advanced context manager utilities. ExitStack manages a dynamic stack of context managers — useful when the number of managers is not known until runtime. nullcontext is a no-op context manager useful for optional context management. AsyncExitStack and asynccontextmanager provide async equivalents.

Pythonexitstack.py
from contextlib import ExitStack, nullcontext

# ExitStack — dynamic number of context managers
def process_files(filenames):
    with ExitStack() as stack:
        files = [stack.enter_context(open(f)) for f in filenames]
        # All files opened; all will be closed on exit regardless of exceptions
        return [f.read() for f in files]

# nullcontext — conditional context management
def process(data, output_file=None):
    with (open(output_file, "w") if output_file else nullcontext()) as f:
        result = compute(data)
        if f:
            f.write(result)
        return result

# asynccontextmanager for async with
from contextlib import asynccontextmanager

@asynccontextmanager
async def async_timer():
    import time
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"Async elapsed: {time.perf_counter() - start:.4f}s")

async def main():
    async with async_timer():
        import asyncio
        await asyncio.sleep(0.1)

The Python Language Reference specifies that the with statement desugars to a precise sequence of operations: evaluate the context expression, call __enter__, bind the result to the as target, execute the body, and unconditionally call __exit__ with exception information (or all-None if no exception). This precise specification means the with statement's behaviour is guaranteed to be consistent regardless of what the context manager does internally.

✅ Expert tab complete

  • I can use ExitStack to manage a dynamic number of context managers
  • I can write an async context manager with @asynccontextmanager for use with async with
  • I can trace through the exact desugar: __enter__, body execution, __exit__ call sequence

Continue to Object-Oriented Python →

Sources

1
Python Language Reference §8.5 — The with statement. docs.python.org/3/reference/compound_stmts.html#the-with-statement.
2
Python Standard Library — contextlib. docs.python.org/3/library/contextlib.html.
3
PEP 343 — The "with" Statement. peps.python.org/pep-0343/.
4
Python Reference — Context Manager Types. docs.python.org/3/library/stdtypes.html#context-manager-types.
Source confidence: High Last verified: Primary source: Python Language Reference §8.5