PythonContext Managers (with)

Context Managers (with)

Whenever code acquires a resource — a file handle, a network socket, a database connection, a lock — that resource needs to be released afterwards, no matter what happens in between. If an exception is raised halfway through, cleanup still has to run. Python's with statement exists to make that guarantee automatic, replacing hand-written try/finally boilerplate with a clean, declarative block.

Why with exists: the manual way vs. with

Before context managers, guaranteeing cleanup meant wrapping every risky operation in try/finally by hand. It works, but it's easy to forget, and it gets repetitive fast.

Python
# Before: manual try/finally cleanup
f = open('notes.txt', 'w')
try:
    f.write('Hello, file!')
    f.write('\nSecond line.')
finally:
    f.close()  # must remember this, even if write() raises

The with statement does the exact same thing — open, use, guarantee close — but the cleanup is baked into the object itself, so you can't forget it.

Python
# After: the with statement
with open('notes.txt', 'w') as f:
    f.write('Hello, file!')
    f.write('\nSecond line.')
# f.close() has already been called here, automatically —
# even if an exception had been raised inside the block
Tip
Most resource-owning classes in the standard library — files, threading.Lock, sqlite3 connections, socket objects — already implement the context manager protocol. You rarely need to write your own; reach for the built-in `with` support first.
The classic example: reading a file

The most common use of with is file handling. Opening a file inside a with block guarantees the file is closed as soon as the block ends, whether it finished normally, returned early, or raised an exception.

Python
with open('notes.txt', 'r') as f:
    contents = f.read()
    print(contents)

# f.closed is True out here — the file was closed automatically
print(f.closed)
Hello, file!
Second line.
True
Writing a custom context manager (a class)

Any object can be used with with as long as it implements two special methods: __enter__ and __exit__. __enter__ runs when the block is entered and its return value is what gets bound to the as variable. __exit__ runs when the block ends — whether normally or via an exception — and is where cleanup belongs.

Python
import time


class Timer:
    """Context manager that measures how long a block takes to run."""

    def __enter__(self):
        self.start = time.perf_counter()
        return self  # this becomes the 'as' variable

    def __exit__(self, exc_type, exc_value, traceback):
        self.elapsed = time.perf_counter() - self.start
        print(f'Elapsed: {self.elapsed:.4f}s')
        return False  # do not suppress exceptions

Python
with Timer() as t:
    total = sum(i * i for i in range(1_000_000))

print('Sum computed:', total)
Elapsed: 0.0821s
Sum computed: 333332833333500

__exit__ always receives three arguments describing any exception that occurred inside the block: exc_type, exc_value, and traceback. If the block completed without error, all three are None. If __exit__ returns a truthy value (like True), Python treats the exception as handled and suppresses it — the with block exits quietly instead of propagating the error. Returning False (or nothing, since None is falsy) lets the exception continue propagating normally, which is almost always what you want.

Warning
Only suppress exceptions in __exit__ when you deliberately want to swallow certain error types (logging them and moving on, for example). Silently returning True for every exception hides real bugs and makes debugging much harder.
The simpler way: @contextmanager

Writing a full class with __enter__ and __exit__ is verbose for simple cases. The contextlib.contextmanager decorator lets you write a context manager as a single generator function instead: code before yield is the setup, code after yield (in a finally block) is the cleanup.

Python
from contextlib import contextmanager
import time


@contextmanager
def timer():
    start = time.perf_counter()
    try:
        yield  # control passes to the with-block here
    finally:
        elapsed = time.perf_counter() - start
        print(f'Elapsed: {elapsed:.4f}s')

Python
with timer():
    total = sum(i * i for i in range(1_000_000))

print('Sum computed:', total)

This produces the same behavior as the Timer class above, but with far less code. The try/finally around yield is what guarantees the cleanup code still runs even if the with block raises an exception — the exception is raised at the yield point inside the generator, so finally always executes.

Multiple context managers in one statement

When you need more than one resource at once — copying from one file to another, for example — you can combine multiple context managers in a single with statement using commas. This is equivalent to nesting them, but reads more clearly.

Python
with open('source.txt', 'r') as src, open('destination.txt', 'w') as dst:
    dst.write(src.read())
# both files are closed automatically here, in reverse order of opening
Note
Each context manager's __exit__ runs even if a later one in the same with statement fails to enter — Python unwinds them safely in reverse order, the same way nested with blocks would.
Summary
  • with guarantees cleanup code runs, even when exceptions occur — no more forgetting a finally

  • Any class implementing __enter__ and __exit__ can be used with with

  • __exit__(self, exc_type, exc_value, traceback) can inspect or suppress exceptions by returning a truthy value

  • @contextmanager from contextlib turns a generator function into a context manager using yield

  • Multiple resources can share one with statement using commas