DocsHub
Advanced

Context Managers

Learn how the with statement works, how to build custom context managers, and when to use them.

Context Managers

You have been using context managers since the file handling section:

with open("file.txt", "r") as f:
    content = f.read()

The with statement is clean and safe — the file closes automatically when the block ends, even if an exception occurs. But how does this actually work? And how do you build your own?

That is what this file covers.


The problem context managers solve

Without a context manager, you have to manage setup and cleanup manually:

file = open("data.txt", "r")
try:
    content = file.read()
    process(content)
finally:
    file.close()   # must always close — even if process() crashes

This works but it is verbose. Every resource you open needs this pattern. Miss the finally once and you have a resource leak.

The with statement wraps this pattern up:

with open("data.txt", "r") as f:
    content = f.read()
    process(content)
# file is closed here automatically — no matter what

Same safety. Half the code.


How with works under the hood

no exception exception raised returns True returns False or None with statement starts Call __enter__()returns the resource Run the with block Call __exit__(None, None, None) Call __exit__(exc_type, exc_val, exc_tb) Continue normally Exception suppressedContinue normally Exception propagates

The with statement calls two special methods:

  • __enter__() — runs at the start of the with block, returns the value bound to as
  • __exit__() — runs at the end of the block, always — whether it finished normally or crashed

Building a class-based context manager

Implement __enter__ and __exit__:

class ManagedFile:
    def __init__(self, filepath, mode="r"):
        self.filepath = filepath
        self.mode = mode
        self.file = None

    def __enter__(self):
        print(f"Opening {self.filepath}")
        self.file = open(self.filepath, self.mode, encoding="utf-8")
        return self.file   # this is what 'as f' receives

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Closing {self.filepath}")
        if self.file:
            self.file.close()
        return False   # do not suppress exceptions


with ManagedFile("notes.txt") as f:
    content = f.read()
    print(content)

Output:

Opening notes.txt
(file contents here)
Closing notes.txt

The three parameters of __exit__ carry exception information:

ParameterMeaning
exc_typeThe exception class — ValueError, TypeError, etc.
exc_valThe exception instance — the actual error object
exc_tbThe traceback object

If no exception occurred, all three are None.

Returning True from __exit__ suppresses the exception:

def __exit__(self, exc_type, exc_val, exc_tb):
    if self.file:
        self.file.close()
    if exc_type is FileNotFoundError:
        print(f"File not found — handled.")
        return True   # suppress FileNotFoundError
    return False      # let everything else propagate

Only suppress exceptions deliberately and carefully. Suppressing an exception means the caller never knows something went wrong. Use it only when you have genuinely handled the problem.


A real class-based example — database connection

import sqlite3

class DatabaseConnection:
    def __init__(self, db_path):
        self.db_path = db_path
        self.connection = None
        self.cursor = None

    def __enter__(self):
        self.connection = sqlite3.connect(self.db_path)
        self.cursor = self.connection.cursor()
        return self.cursor

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            # no exception — commit the transaction
            self.connection.commit()
        else:
            # exception occurred — roll back
            self.connection.rollback()
            print(f"Transaction rolled back due to: {exc_val}")

        self.cursor.close()
        self.connection.close()
        return False


# clean usage
with DatabaseConnection("users.db") as cursor:
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER, name TEXT)")
    cursor.execute("INSERT INTO users VALUES (1, 'Ali')")
    cursor.execute("INSERT INTO users VALUES (2, 'Sara')")
# automatically committed and closed

If an exception happens inside the with block, the transaction rolls back automatically. If everything succeeds, it commits. The caller does not need to think about any of this.


Building with contextlib — the function-based way

Python's contextlib module gives you a decorator that turns a generator function into a context manager. This is often cleaner than writing a full class:

from contextlib import contextmanager

Structure:

@contextmanager
def my_context():
    # setup — runs before the with block
    print("Setting up")
    
    try:
        yield value   # 'value' is what 'as x' receives
    finally:
        # cleanup — always runs
        print("Cleaning up")

Everything before yield is __enter__. Everything after is __exit__. The finally ensures cleanup always happens.


Simple example

from contextlib import contextmanager

@contextmanager
def managed_file(filepath, mode="r"):
    print(f"Opening {filepath}")
    f = open(filepath, mode, encoding="utf-8")
    try:
        yield f
    finally:
        f.close()
        print(f"Closed {filepath}")


with managed_file("notes.txt") as f:
    print(f.read())

Output:

Opening notes.txt
(file contents)
Closed notes.txt

Real example 1 — a timer context manager

Measure how long a block of code takes:

import time
from contextlib import contextmanager

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


with timer("Sorting 1 million numbers"):
    data = list(range(1_000_000, 0, -1))
    data.sort()

with timer("Sum of squares"):
    result = sum(x ** 2 for x in range(1_000_000))

Output:

Sorting 1 million numbers: 0.0812s
Sum of squares: 0.0634s

Real example 2 — temporary directory

Create a temporary directory, do work inside it, then clean it up automatically:

import shutil
import tempfile
from pathlib import Path
from contextlib import contextmanager

@contextmanager
def temp_directory():
    path = Path(tempfile.mkdtemp())
    print(f"Created temp dir: {path}")
    try:
        yield path
    finally:
        shutil.rmtree(path)
        print(f"Deleted temp dir: {path}")


with temp_directory() as tmpdir:
    # do work in the temporary directory
    output_file = tmpdir / "results.txt"
    output_file.write_text("Processing results...", encoding="utf-8")
    print(f"Wrote to: {output_file}")
    print(f"Contents: {output_file.read_text()}")

# temp directory is gone now

Output:

Created temp dir: /tmp/tmpabc123
Wrote to: /tmp/tmpabc123/results.txt
Contents: Processing results...
Deleted temp dir: /tmp/tmpabc123

Real example 3 — suppressing specific exceptions

contextlib has a built-in context manager for suppressing exceptions:

from contextlib import suppress

# instead of this
try:
    import ujson as json
except ImportError:
    import json

# you can write this
with suppress(ImportError):
    import ujson as json

Another use — delete a file if it exists, ignore if it does not:

from contextlib import suppress
from pathlib import Path

with suppress(FileNotFoundError):
    Path("temp_file.txt").unlink()

No try/except needed. Clean and readable.


Real example 4 — redirecting stdout

contextlib can redirect print output to a file or string:

import io
from contextlib import redirect_stdout

# capture print output into a string
buffer = io.StringIO()

with redirect_stdout(buffer):
    print("This goes to the buffer, not the screen")
    print("So does this")

output = buffer.getvalue()
print(f"Captured: {output!r}")

Output:

Captured: 'This goes to the buffer, not the screen\nSo does this\n'

Useful for testing functions that print output.


Stacking context managers

You can use multiple context managers in one with statement:

# old way — nested
with open("input.txt", "r") as infile:
    with open("output.txt", "w") as outfile:
        outfile.write(infile.read())

# modern way — on one line
with open("input.txt", "r") as infile, open("output.txt", "w") as outfile:
    outfile.write(infile.read())

Both files are managed together. If either fails, both are cleaned up.


contextlib.ExitStack — dynamic context managers

Sometimes you do not know how many context managers you need until runtime. ExitStack handles this:

from contextlib import ExitStack

files = ["file1.txt", "file2.txt", "file3.txt"]

with ExitStack() as stack:
    handles = [
        stack.enter_context(open(f, "r", encoding="utf-8"))
        for f in files
    ]
    for handle in handles:
        print(handle.read())
# all files closed here

You push as many context managers as you need onto the stack. All of them get cleaned up when the with block ends.


When to write a context manager

SituationWrite a context manager?
Opening and closing a resourceYes
Setup and teardown that always go togetherYes
Temporarily changing state and restoring itYes
Measuring time of a blockYes
Any pattern that needs guaranteed cleanupYes
Simple one-off code with no cleanupNo

The core idea — any time you find yourself writing try/finally for cleanup, a context manager is usually the cleaner solution.


Summary

ConceptExample
Use a context managerwith open(...) as f:
Class-basedImplement __enter__ and __exit__
Function-based@contextmanager with yield
Suppress exceptionswith suppress(ErrorType):
Stack multiplewith A() as a, B() as b:
Dynamic stackingwith ExitStack() as stack:
Suppress in __exit__return True
Propagate in __exit__return False

On this page