LearnContact
Lesson 5725 min read

Context Managers

Learn how Python context managers guarantee setup and cleanup code always runs, using the with statement, __enter__/__exit__, and the @contextmanager decorator.

Introduction

Whenever your code opens a file, connects to a database, or acquires a lock, it needs to release that resource afterward — no matter what happens in between. Forgetting to do this "cleanup" step is one of the most common sources of bugs: files that stay locked, connections that leak, and programs that slowly run out of resources.

Python solves this problem with context managers: objects that know how to set something up and guarantee it gets cleaned up afterward, even if an error occurs partway through. You have already used one without necessarily naming it — the with statement when opening files. In this lesson, you will learn exactly how that mechanism works and how to build your own.

What You Will Learn
  • The cleanup problem context managers solve.
  • How the with statement works behind the scenes.
  • The __enter__ and __exit__ dunder methods.
  • How to write a context manager as a class.
  • How to write one quickly using @contextmanager.
  • Other common built-in context managers.

The Problem: Guaranteed Cleanup

Imagine opening a file manually, without a context manager. You must remember to call close() yourself — and if an error happens before that line, close() never runs.

without_context_manager.py
f = open("notes.txt", "w")
f.write("Hello")
result = 10 / 0   # an error happens here
f.close()          # this line never runs!
Output
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
# notes.txt is left open — close() was skipped

You could fix this with a try/finally block, ensuring close() always runs, but writing that every single time is repetitive and easy to forget.

with_try_finally.py
f = open("notes.txt", "w")
try:
    f.write("Hello")
    result = 10 / 0
finally:
    f.close()   # this always runs, even after an error
Output
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
# but this time f.close() ran before the error propagated

Context managers exist to package up exactly this "always clean up" pattern into something reusable and much shorter to write.

The with Statement Recap

You have already seen the with statement used for file handling. It automatically closes the file for you once the indented block finishes, whether the block succeeds or raises an error.

with_recap.py
with open("notes.txt", "w") as f:
    f.write("Hello")
    result = 10 / 0

print("File is guaranteed to be closed here")
Output
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
# f is closed automatically before the error propagates
# "File is guaranteed to be closed here" never prints because
# the exception stops execution first — but the file was still closed

The with statement is not magic reserved only for open(). It works with any object that follows the context manager protocol — which you can implement yourself.

How with Works Under the Hood

A context manager is simply an object that defines two special dunder methods: __enter__ and __exit__.

__enter__(self)

Runs when the with block starts. Its return value is what gets assigned to the "as" variable.

__exit__(self, exc_type, exc_val, exc_tb)

Runs when the with block ends — whether it finished normally or raised an exception. This is where cleanup happens.

Automatic Call

Python calls __enter__ before the block and __exit__ after, without you writing try/finally yourself.

In other words, this code:

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

is roughly equivalent to Python doing this behind the scenes:

manager = open("notes.txt")
f = manager.__enter__()
try:
    data = f.read()
finally:
    manager.__exit__(None, None, None)

Writing a Custom Context Manager (Class)

You can create your own context manager by writing a class with __enter__ and __exit__ methods. Here is a simple one that logs when a resource opens and closes.

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

    def __enter__(self):
        print(f"Opening {self.filename}")
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Closing {self.filename}")
        self.file.close()


with ManagedFile("notes.txt", "w") as f:
    f.write("Hello, context managers!")
Output
Opening notes.txt
Closing notes.txt

__enter__ runs first and returns the file object, which gets bound to f. Once the indented block finishes, __exit__ automatically runs and closes the file — no matter what happened inside the block.

Handling Exceptions in __exit__

If an error occurs inside the with block, Python passes information about it to __exit__ through the exc_type, exc_val, and exc_tb parameters. This lets your context manager react to — or even suppress — the error.

exit_with_exception_info.py
class SafeDivision:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is ZeroDivisionError:
            print(f"Caught an error: {exc_val}")
            return True   # True means: suppress the exception
        return False       # False (or None) means: let it propagate


with SafeDivision():
    print(10 / 0)

print("Program continues normally")
Output
Caught an error: division by zero
Program continues normally
Return Value of __exit__
  • Returning True (or any truthy value) tells Python the exception was handled — it will not propagate further.
  • Returning False, None, or nothing at all lets the exception continue as normal.
  • Most context managers should only suppress exceptions when they have a good reason to.

The @contextmanager Decorator

Writing a full class with __enter__ and __exit__ is powerful, but often overkill for simple cases. The contextlib module provides a @contextmanager decorator that lets you write a context manager as a single generator function using yield.

managed_file_generator.py
from contextlib import contextmanager

@contextmanager
def managed_file(filename, mode):
    print(f"Opening {filename}")
    f = open(filename, mode)
    try:
        yield f
    finally:
        print(f"Closing {filename}")
        f.close()


with managed_file("notes.txt", "w") as f:
    f.write("Hello again!")
Output
Opening notes.txt
Closing notes.txt

Everything before yield acts like __enter__ — it runs when the with block starts. The yielded value becomes the "as" variable. Everything after yield (inside finally) acts like __exit__ — it runs when the block ends, guaranteed, because it lives in a finally clause.

timer_context_manager.py
import time
from contextlib import contextmanager

@contextmanager
def timer(label):
    start = time.time()
    yield
    elapsed = time.time() - start
    print(f"{label} took {elapsed:.4f} seconds")


with timer("Sum calculation"):
    total = sum(range(1_000_000))

print("Total:", total)
Output
Sum calculation took 0.0123 seconds
Total: 499999500000
Class vs. @contextmanager
  • Use a class when your context manager needs to store state or be reused across many with blocks.
  • Use @contextmanager for quick, simple, single-purpose setup/teardown logic.
  • Both approaches implement exactly the same protocol — pick whichever reads more clearly.

Other Built-in Context Managers

The with statement is used throughout Python's standard library wherever a resource needs guaranteed cleanup.

open()

Guarantees files are closed automatically, even if reading or writing raises an error.

threading.Lock()

Used with "with lock:" to guarantee a lock is always released, avoiding deadlocks between threads.

database connections

Libraries like sqlite3 use context managers so connections and transactions are properly closed or rolled back.

tempfile.TemporaryFile()

Creates a temporary file that is automatically deleted once the with block ends.

lock_example.py
import threading

lock = threading.Lock()

def update_shared_value():
    with lock:
        # only one thread can be inside this block at a time
        print("Updating safely...")

update_shared_value()
Output
Updating safely...

Multiple Context Managers

You can open more than one context manager in a single with statement by separating them with commas. This is often used to work with two files at once.

multiple_with.py
with open("source.txt", "w") as src, open("backup.txt", "w") as bkp:
    src.write("original data")
    bkp.write("original data")

print("Both files closed automatically")
Output
Both files closed automatically

Common Mistakes

Avoid These Mistakes
  • Forgetting the finally in a @contextmanager generator — without it, an exception before yield's cleanup code will skip your cleanup entirely.
  • Yielding more than once inside a @contextmanager function — it only supports a single yield.
  • Returning True from __exit__ by accident, which silently swallows real errors.
  • Manually calling open()/close() instead of using with, reintroducing the exact bug context managers solve.
  • Assuming __exit__ always means "no error happened" — always check exc_type when you need to react to failures.

Best Practices

  • Prefer with over manual open()/close() or acquire()/release() pairs whenever possible.
  • Use @contextmanager for short, simple setup/teardown logic.
  • Use a class-based context manager when you need to store configuration or reuse the manager many times.
  • Only suppress exceptions in __exit__ (by returning True) when you have a specific, intentional reason to.
  • Keep the code inside a with block focused — cleanup should not depend on anything the block might fail to set.

Frequently Asked Questions

Do I always need a class to make a context manager?

No. The @contextmanager decorator from contextlib lets you write one as a simple generator function using yield, which is enough for most use cases.

What happens if I forget __exit__ in a class?

Python will raise an AttributeError when the with block ends, because it cannot find __exit__ to call. Both __enter__ and __exit__ are required for the protocol to work.

Can a context manager be used without the with statement?

You could call __enter__ and __exit__ manually, but that defeats the purpose — the whole benefit is that with guarantees __exit__ runs automatically, even during an error.

Is a context manager the same as a decorator?

No, though @contextmanager is a decorator that turns a generator function into a context manager. The with statement and the context manager protocol are separate from Python's general decorator syntax.

Why does yield only appear once in a @contextmanager function?

The code before yield is the setup (__enter__) and the code after yield is the cleanup (__exit__). A single yield cleanly separates those two phases; multiple yields would break that structure.

Key Takeaways

  • Context managers guarantee cleanup code runs, even if an error occurs.
  • The with statement calls __enter__ before the block and __exit__ after it, automatically.
  • You can write a context manager as a class with __enter__ and __exit__ methods.
  • The @contextmanager decorator from contextlib lets you write one quickly using a generator function and yield.
  • Built-in examples include open(), threading.Lock(), and database connections.
  • Multiple context managers can be combined in one with statement using commas.

Summary

Context managers turn the repetitive "set up, then always clean up" pattern into something Python handles automatically through the with statement. Whether you write a full class with __enter__ and __exit__, or a quick generator function with @contextmanager, the result is the same: resources like files, locks, and connections are always released properly, even when errors happen.

Lesson 57 Completed
  • You understand the cleanup problem context managers solve.
  • You know how __enter__ and __exit__ work behind the scenes of with.
  • You can write a custom context manager as a class.
  • You can write one quickly using @contextmanager and yield.
  • You are ready to learn how to test your Python code automatically.
Next Lesson →

Unit Testing