Skip to content
C Codeloom
Python

Python Context Managers and the with Statement

A practical guide to Python's with statement and context managers — why they exist, how to write your own with __enter__ and __exit__, and how to use contextlib for one-liners.

·4 min read · By Codeloom
Beginner 9 min read

What you'll learn

  • Why the with statement exists
  • How __enter__ and __exit__ work
  • Writing a class-based context manager
  • Using contextlib.contextmanager for simpler cases
  • Combining multiple context managers

Prerequisites

  • Basic Python familiarity

A context manager is any object that knows how to set something up and tear it down again — opening and closing a file, acquiring and releasing a lock, beginning and committing a database transaction. The with statement is the syntax Python uses to drive that lifecycle so you cannot forget the teardown step.

The Problem with Manual Cleanup

Consider opening a file without with.

f = open("data.txt")
data = f.read()
f.close()

This usually works. It breaks the moment f.read() raises an exception, because f.close() is skipped and the file handle leaks until the garbage collector eventually closes it. The fix is try/finally.

f = open("data.txt")
try:
    data = f.read()
finally:
    f.close()

That works but it is verbose and easy to get wrong. The with statement bakes the same pattern into a one-liner.

with open("data.txt") as f:
    data = f.read()

When the block exits — normally or via exception — the file is closed. That’s it.

How with Actually Works

A context manager is just an object that implements two dunder methods: __enter__ and __exit__. The with statement calls __enter__ at the top of the block and __exit__ when the block ends.

Here is a minimal example.

class Loud:
    def __enter__(self):
        print("entering")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("exiting")
        return False  # don't swallow exceptions

with Loud() as ctx:
    print("inside block")

The output is entering, inside block, exiting. If you raise inside the block, __exit__ still runs.

The three arguments to __exit__ describe an exception that occurred inside the block (or are all None if everything went fine). Returning True from __exit__ swallows the exception, which is rarely what you want — return False or simply nothing.

A Realistic Example: a Timer

A class-based context manager for timing a block of code.

import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self.start
        print(f"Block took {self.elapsed:.4f}s")

with Timer() as t:
    sum(range(1_000_000))

After the block, t.elapsed holds the duration. Notice how __enter__ returns self, which is what as t captures.

The contextlib Shortcut

For simple managers, writing a whole class is overkill. The contextlib.contextmanager decorator lets you build one from a generator.

from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"Block took {elapsed:.4f}s")

with timer():
    sum(range(1_000_000))

Code before yield runs as __enter__. Code after yield (in the finally block) runs as __exit__. Whatever you yield becomes the value bound by as.

This style is shorter, easier to read, and perfect for cases that don’t need their own class.

Managing Other Resources

Context managers shine far beyond files. Locks, database connections, temporary directories, and even mocks in tests all expose context manager interfaces.

import threading
lock = threading.Lock()

with lock:
    # critical section — lock released automatically
    shared_counter += 1
import tempfile

with tempfile.TemporaryDirectory() as tmp:
    # tmp is a path; the directory and its contents are deleted on exit
    ...

The pattern is consistent: acquire on entry, release on exit, no matter what happens in between.

Combining Multiple Managers

You can stack managers in one with statement using commas.

with open("input.txt") as src, open("output.txt", "w") as dst:
    for line in src:
        dst.write(line.upper())

Both files are guaranteed to close. If you need to manage a dynamic number of managers, contextlib.ExitStack lets you push them on programmatically.

from contextlib import ExitStack

paths = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
    files = [stack.enter_context(open(p)) for p in paths]
    # all files closed when the with block exits

Suppressing Exceptions Cleanly

contextlib.suppress is a tiny context manager that swallows a specific exception type. It is cleaner than an empty except block.

from contextlib import suppress
import os

with suppress(FileNotFoundError):
    os.remove("maybe-here.txt")

If the file doesn’t exist, nothing happens. If it does, it gets removed.

When to Reach for a Custom Context Manager

Any time you have a resource that needs deterministic cleanup, or a paired setup/teardown action — opening a network connection, starting and stopping a profiler, switching directories, patching configuration during a test — a context manager makes the intent obvious and the cleanup automatic.

Wrapping Up

with is one of Python’s most underrated features. Once you start writing your own context managers — either as classes or via contextlib.contextmanager — you stop worrying about forgotten cleanup and your code reads as a list of named scopes instead of a maze of try/finally.