Chapters
Python114 chapters

Files and errorsChapter 86 of 114

Context Managers

Guarantee that cleanup happens, even when something goes wrong.

What with actually guarantees

with runs setup, then your block, then cleanup — and the cleanup runs whether the block finished, returned, or raised:

Python
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("one")

print(f.closed)

Output

True

The alternative is a try/finally you have to remember to write:

Python
f = open("notes.txt", "w", encoding="utf-8")
try:
    f.write("one")
finally:
    f.close()

print(f.closed)

Output

True

with is that, in one line, with no way to forget the finally.

Cleanup happens even on an exception

Python
try:
    with open("notes.txt", "w", encoding="utf-8") as f:
        f.write("partial")
        raise ValueError("something went wrong")
except ValueError as problem:
    print("caught:", problem)

print("file closed:", f.closed)

Output

caught: something went wrong
file closed: True

Writing one with a class

Two methods. __enter__ returns whatever the as name should be; __exit__ does the cleanup:

Python
class Section:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print("start", self.name)
        return self

    def __exit__(self, exc_type, exc, tb):
        print("end", self.name)
        return False

with Section("build") as section:
    print("working on", section.name)

Output

start build
working on build
end build

__exit__ receives the exception if there was one, and its return value decides whether to suppress it. Return False (or nothing) to let it continue. Returning True swallows it, which is almost never what you want:

Python
class Quiet:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        print("swallowing", exc_type.__name__)
        return True

with Quiet():
    raise ValueError("gone")

print("still running")

Output

swallowing ValueError
still running

The easier way: @contextmanager

For most cases a generator is shorter than a class. Everything before yield is setup, everything after is cleanup:

Python
from contextlib import contextmanager

@contextmanager
def section(name):
    print("start", name)
    try:
        yield name
    finally:
        print("end", name)

with section("build") as label:
    print("working on", label)

Output

start build
working on build
end build

The try/finally matters. Without it, an exception in the block would skip the cleanup:

Python
from contextlib import contextmanager

@contextmanager
def section(name):
    print("start", name)
    try:
        yield
    finally:
        print("end", name)

try:
    with section("build"):
        raise ValueError("failed")
except ValueError:
    print("caught outside")

Output

start build
end build
caught outside

A practical one

Temporarily changing something and putting it back is the classic use:

Python
import os
from contextlib import contextmanager

@contextmanager
def env(name, value):
    old = os.environ.get(name)
    os.environ[name] = value
    try:
        yield
    finally:
        if old is None:
            del os.environ[name]
        else:
            os.environ[name] = old

print(os.environ.get("MODE"))
with env("MODE", "test"):
    print(os.environ.get("MODE"))
print(os.environ.get("MODE"))

Output

None
test
None

Several at once

Python
with open("a.txt", "w", encoding="utf-8") as a, open("b.txt", "w", encoding="utf-8") as b:
    a.write("first")
    b.write("second")

print(a.closed, b.closed)

Output

True True

Both close, in reverse order, even if the second open fails.

suppress, for the exception you truly do not care about

Python
import os
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("never-existed.txt")

print("carried on")

Output

carried on

That is a tidier try/except/pass, and unlike a bare except it names exactly what it ignores.

Test yourself

2 questions

When does __exit__ run?

Show the answer

Always, including when the block raises — That guarantee is the whole reason with exists, and why you never need to remember a finally.

In a @contextmanager generator, why wrap the yield in try/finally?

Show the answer

Otherwise an exception in the block skips the cleanup — Everything before the yield is setup and everything after is cleanup, but only finally guarantees the cleanup runs.

Next chapter

Paths with pathlib

Build, inspect and search file paths without worrying about slashes.