A context manager in Python: opening and closing without forgetting

A context manager sets a resource up when a with block starts and closes it on the way out, even when an error cuts the work short.
5 min read
Believemy logo

Definition

A program that opens a file takes on a debt: it will have to close that file. As long as everything goes well, the debt is settled a few lines below and nobody thinks about it. But if an error shows up in the middle of the work, execution jumps over the closing line and the file stays open behind the program's back. On a script you run by hand, this never shows; on a server running for weeks, abandoned resources pile up until something breaks.

A context manager answers that annoyance. It is an object that sets a resource up when the program enters a block and puts it back in order when the program leaves. It is used with the with keyword, and its promise is simple: the cleanup will happen, whether the block ends normally or fails halfway through.

PYTHON
with open("report.txt", "w") as report:
    report.write("Monthly figures")

# Here, the file is already closed

These lines do more than shorten the code, they move a responsibility: it is no longer up to whoever uses the file to remember the closing step, it is up to the file object to guarantee it. Otherwise you would have to close inside a finally and rebuild that scaffolding everywhere the resource is used.


The mechanism, no magic involved

How does with know what to close, when it knows nothing about files? It knows nothing, precisely: it calls two methods the object has to provide, __enter__ and __exit__, two members of the family described by the term magic method. Any object exposing them becomes usable inside a with block, be it a file, a database connection or a plain stopwatch.

The table below walks through a block from start to finish: the moment, what Python triggers on its own, and what you see of it.

MomentWhat Python callsWhat you see of it
Entering the block__enter__The value received after as
Body of the blocknothingYour code, unchanged
Normal exit__exit__(None, None, None)The cleanup, silently
Exit on error__exit__(cls, value, trace)The cleanup, then the error rising

The last row is the most instructive one. When the block fails, Python hands __exit__ three arguments describing the incident: the class of the error, the raised object and the call trace. All three hold None when everything went well.

The method therefore knows which situation it is working in, which makes the protocol useful beyond cleanup: commit a transaction when the block succeeded, roll it back when it failed.


Writing your own

Nothing forces you to settle for the managers shipped with Python. Two roads lead to the same result: a class implementing both reserved methods, or a generator turned into a manager by a decorator from the standard library, shorter and far more common.

PYTHON
from contextlib import contextmanager
import time

@contextmanager
def stopwatch(step):
    start = time.perf_counter()      # before the yield: the role of __enter__
    try:
        yield                        # the body of the with block runs here
    finally:
        spent = time.perf_counter() - start
        print(step, "took", round(spent, 3), "seconds")

with stopwatch("loading"):
    load_the_data()

The convention reads in one go: what comes before the yield plays the part of __enter__, what comes after it the part of __exit__, and the yield marks the spot where the body of the block slots in.

The try is not decoration. If the block raises, Python throws the error back inside the generator, at the line of the yield, where the function would stop. Without finally, the stopwatch would stay silent on the very day its measurement matters most, the day loading went wrong.

Good to know

The contextlib module does not stop at that decorator: it also offers suppress, which deliberately ignores one named error, and ExitStack, which stacks resources whose number is only known at runtime.


Two traps worth an evening

The first comes from one return too many. An __exit__ method returning a truthy value tells Python that the error has been handled: the exception stops dead, with no rising and no trace in the logs.

PYTHON
class Connection:
    def __exit__(self, cls, value, trace):
        self.close()
        return True   # swallows every error raised in the block

# Correct: return nothing at all, or return False
Warning

Returning True out of habit turns a context manager into a rug under which incidents get swept: the program carries on with a half-written transaction, and the defect surfaces hours later, far from its cause. Swallow an error only when you know which one it is, and why.

The second trap concerns the variable named after as. It does not vanish at the end of the block, because Python creates no local scope here. What vanished is the resource behind the name: reading the file one line below raises a ValueError, while the variable itself looks perfectly valid. The habit to build: everything that needs the resource lives inside the block.


Frequently asked questions

Question

Can several resources be opened in a single block?

Yes, by separating them with commas, and since Python 3.10 by wrapping them in parentheses to spread them over several lines. Closing happens in the reverse order of opening, which matters as soon as the second resource depends on the first.

Question

Is a try/except still needed around the block?

Yes, as soon as the error has to be handled. The protocol guarantees the cleanup, not the rescue: the exception rises all the same once the closing is done, and except remains the only place to decide what to do about it. The two mechanisms answer two different questions.

Question

Is there a version for asynchronous code?

Yes: async with, which calls __aenter__ and __aexit__ instead of the two usual methods, and allows waiting during the opening as well as during the closing. Essential when the resource is a network session that is not released instantly.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.