Context Managers in Python | Python Course | Dataplexa

Context Managers in Python

Every time you open a file with with open(...) as f:, you are using a context manager. Context managers guarantee that setup and teardown logic runs reliably — even when exceptions occur. They are the cleanest solution to the resource management problem: ensuring files are closed, database connections released, locks freed, and temporary state restored, no matter what happens inside the block.

This lesson covers how the with statement works, how to build context managers using classes and the contextlib module, and the real-world scenarios where they prevent resource leaks.

The Problem Context Managers Solve

Without a context manager, resource cleanup depends on the programmer remembering to do it — and on no exception occurring before the cleanup code runs.

# Without context manager — fragile cleanup

f = open("data.txt", "w")
f.write("some data")
f.close()   # fine if nothing goes wrong

# If an exception occurs before close(), the file stays open forever
f = open("data.txt", "w")
try:
    f.write("some data")
    raise ValueError("something went wrong")   # simulated error
finally:
    f.close()   # must wrap everything in try/finally to guarantee cleanup
    print("File closed in finally block")
File closed in finally block

The with statement replaces the try/finally pattern with a single clean line — teardown is guaranteed automatically.

How the with Statement Works

The with statement relies on two magic methods: __enter__ runs at the start of the block, __exit__ runs at the end — guaranteed even if an exception occurs. Any object that implements these two methods is a context manager.

# with statement — guaranteed setup and teardown

with open("data.txt", "w") as f:
    f.write("Hello from context manager!")
# f.close() is called automatically here — exception or not

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

# Multiple context managers on one line (Python 3.1+)
with open("input.txt", "w") as inp, open("output.txt", "w") as out:
    inp.write("input data")
    out.write("output data")
    print("Both files written and closed automatically.")

# Python 3.10+ parenthesised form — cleaner for 3+ managers
# with (
#     open("a.txt") as a,
#     open("b.txt") as b,
# ): ...
Hello from context manager! Both files written and closed automatically.
  • The as f part binds the value returned by __enter__ to the variable f.
  • Multiple context managers can be combined on one line with a comma.
  • __exit__ is always called — even if an exception is raised inside the block.

Class-Based Context Manager

Any class that implements __enter__ and __exit__ is a context manager. This gives full control over setup, teardown, and exception handling.

class ManagedFile:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode     = mode

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        print(f"Opened: {self.filename}")
        return self.file            # bound to the 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()
        print(f"Closed: {self.filename}")
        if exc_type:
            print(f"Exception handled: {exc_val}")
        return False   # False = let exception propagate (normal default)

# Normal use — no exception
with ManagedFile("notes.txt", "w") as f:
    f.write("context managers are great")
    print("Write done.")

# Exception inside the block — __exit__ still runs
try:
    with ManagedFile("notes.txt", "a") as f:
        raise ValueError("oops!")
except ValueError:
    print("Exception propagated (return False in __exit__)")
Opened: notes.txt Write done. Closed: notes.txt Opened: notes.txt Closed: notes.txt Exception handled: oops! Exception propagated (return False in __exit__)
  • __exit__ receives: exc_type, exc_val, exc_tb — all None when no exception occurred.
  • Return True to suppress the exception; return False (or nothing) to let it propagate.
  • In most real-world cases you want exceptions to propagate — only suppress when you have a specific reason.

Generator-Based Context Manager — @contextmanager

contextlib.contextmanager lets you write a context manager as a generator function. Everything before yield is setup, the yield value is bound to as, and everything after is teardown.

from contextlib import contextmanager

@contextmanager
def managed_file(filename, mode):
    print(f"Opening: {filename}")
    f = open(filename, mode)
    try:
        yield f           # value bound to the 'as' variable
    except Exception as e:
        print(f"Exception inside block: {e}")
        raise             # re-raise — don't suppress
    finally:
        f.close()         # always runs
        print(f"Closed: {filename}")

with managed_file("notes.txt", "w") as f:
    f.write("generator-based context manager")
    print("Write done.")
Opening: notes.txt Write done. Closed: notes.txt
  • The try/finally inside the generator ensures teardown runs even if the block raises an exception.
  • Only one yield is allowed — it marks the boundary between setup and teardown.
  • This is the preferred pattern for simple context managers — less code, same guarantees.

Practical — Timer and Directory Change

import time, os
from contextlib import contextmanager

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

@contextmanager
def change_dir(path):
    original = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(original)   # always restore — even on exception

with timer("Sum of range"):
    result = sum(range(5_000_000))
    print("Result:", result)

print("Before:", os.getcwd())
with change_dir("/tmp"):
    print("Inside:", os.getcwd())
print("After:", os.getcwd())
Result: 12499997500000 Sum of range took 0.1823s Before: /home/user/project Inside: /tmp After: /home/user/project

contextlib Utilities

from contextlib import suppress, redirect_stdout, nullcontext, closing
import io

# suppress — silently ignore specific exceptions
with suppress(FileNotFoundError):
    open("nonexistent.txt")
print("Carried on after missing file")

# redirect_stdout — capture print() output into a string
buffer = io.StringIO()
with redirect_stdout(buffer):
    print("this goes to the buffer")
print("Captured:", buffer.getvalue().strip())

# nullcontext — no-op, useful when a context manager is optional
debug_mode = False
ctx = timer("block") if debug_mode else nullcontext()
with ctx:
    x = sum(range(100))

# closing — wraps any object with .close() in a context manager
class Resource:
    def close(self): print("Resource closed")
    def use(self): print("Resource in use")

with closing(Resource()) as r:
    r.use()
Carried on after missing file Captured: this goes to the buffer Resource in use Resource closed
  • suppress(*exceptions) — cleanest way to intentionally swallow a specific exception.
  • redirect_stdout(buffer) — useful in testing to capture output without mocking.
  • nullcontext() — does nothing; use when a context manager is optional in conditional code.
  • closing(obj) — wraps any object with a close() method as a context manager.

Quick Reference Table

ToolApproachBest For
__enter__ / __exit__Class-basedComplex setup/teardown, stateful managers
@contextmanagerGenerator-basedSimple managers — less boilerplate
suppress()contextlib utilityIntentionally swallowing specific exceptions
redirect_stdout()contextlib utilityCapturing print output in tests
nullcontext()contextlib utilityOptional context manager in conditional code
closing()contextlib utilityWrapping any object with .close()

Practice

What two magic methods must a class implement to work as a context manager?



What does returning True from __exit__ do?



In a @contextmanager generator, what does yield mark?



Which contextlib utility silently ignores a specific exception type?



Is __exit__ called when an exception occurs inside the with block?



Which contextlib utility wraps any object that has a close() method?



Quick Quiz

What is the main advantage of using a context manager over a plain try/finally block?





What value is bound to the variable after as in a with statement?





In a @contextmanager generator, why should yield be inside a try/finally?





What are the three arguments __exit__ receives when an exception occurs?





Which approach is preferred for simple context managers that do not need state?





Which contextlib utility lets you ignore a FileNotFoundError without a try/except block?





NEXT UP
Multithreading in Python
Running tasks concurrently using Python's threading module — the GIL, locks, thread pools, and when threads actually help.