Skip to content
Laviyx Learn

Try

Python course menu

Python Context Managers: The with Statement, __enter__/__exit__ and contextlib

Learn Python context managers: why the with statement guarantees clean-up, how __enter__ and __exit__ work, writing your own with a class or @contextmanager, contextlib.suppress, and managing several resources at once.

Advanced10 min readUpdated
On this page

Some resources must always be cleaned up after use: files must be closed, locks released, database transactions committed or rolled back. A context manager handles that automatically, and the with statement is how you use one. The clean-up runs even if an error happens in the middle.

The problem with manual clean-up#

Python
f = open("hello.txt", "w")
try:
    f.write("hi")
finally:
    f.close()
print(f.closed)
Output
True

The try / finally guarantees the file closes, but it is easy to forget and noisy to write every time.

The with statement#

Python
with open("hello.txt", "w") as f:
    f.write("hi")

print(f.closed)
Output
True

Files are context managers. When the with block ends, normally or because of an exception, the file is closed automatically.

How it works: enter and exit#

Any object with these two methods can be used in with:

MethodRunsReturns
__enter__(self)at the start of the with blockthe value bound by as
__exit__(self, exc_type, exc, tb)at the end, alwaysTrue to swallow an exception, otherwise False

A timer as a context manager:

Python
import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        print("timer started")
        return self

    def __exit__(self, exc_type, exc, tb):
        self.elapsed = time.perf_counter() - self.start
        print(f"timer stopped ({exc_type.__name__ if exc_type else 'no error'})")
        return False

with Timer() as t:
    total = sum(range(100000))

print(total, t.elapsed < 1)
Output
timer started
timer stopped (no error)
4999950000 True

Clean-up even on errors#

Python
class Timer:
    def __enter__(self):
        print("enter")
        return self

    def __exit__(self, exc_type, exc, tb):
        print("exit runs even on error:", exc_type.__name__)
        return False

try:
    with Timer():
        1 / 0
except ZeroDivisionError:
    print("error still raised")
Output
enter
exit runs even on error: ZeroDivisionError
error still raised

__exit__ received the exception details and returned False, so the error continued on to the except block. Returning True would have silenced it; only do that deliberately.

The easy way: @contextmanager#

The contextlib module turns a simple generator into a context manager. Code before yield is the "enter" part; code after it is the "exit" part:

Python
from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f"<{name}>")
    yield
    print(f"</{name}>")

with tag("h1"):
    print("Title")
Output
<h1>
Title
</h1>

For guaranteed clean-up, put the yield inside try / finally:

Python
import os
from contextlib import contextmanager

@contextmanager
def working_directory(path):
    old = os.getcwd()
    os.makedirs(path, exist_ok=True)
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(old)          # always switch back

start = os.path.basename(os.getcwd())
with working_directory("reports"):
    print("inside:", os.path.basename(os.getcwd()))
print("back:", os.path.basename(os.getcwd()) == start)
Output
inside: reports
back: True

Handy ready-made context managers#

contextlib.suppress ignores specific exceptions:

Python
import os
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("does-not-exist.txt")

print("no crash")
Output
no crash

Others you will meet: threading.Lock() for locks, database connections and transactions, tempfile.TemporaryDirectory(), and unittest.mock.patch in tests.

Several resources at once#

Python
with open("a.txt", "w") as src, open("b.txt", "w") as dst:
    src.write("A")
    dst.write("B")

print(open("a.txt").read() + open("b.txt").read())
Output
AB

Both files are closed when the block ends, in reverse order.

Common mistakes#

Practice#

Practice exercise

Write a transaction(account) context manager for a dictionary like {"balance": 1000}. If the block raises an exception, restore the balance it had before the block, print rolled back, and re-raise the error. Withdraw 300 successfully, then try an overdraft of 5,000.

Show hint

Take a copy (dict(account)) before yield. In except, restore it with clear() and update(), then raise.

Show solution
Python
from contextlib import contextmanager

@contextmanager
def transaction(account):
    snapshot = dict(account)
    try:
        yield account
    except Exception:
        account.clear()
        account.update(snapshot)
        print("rolled back")
        raise

account = {"balance": 1000}

with transaction(account) as acc:
    acc["balance"] -= 300
print(account)

try:
    with transaction(account) as acc:
        acc["balance"] -= 5000
        if acc["balance"] < 0:
            raise ValueError("overdrawn")
except ValueError as e:
    print("error:", e)
print(account)
Output
{'balance': 700}
rolled back
error: overdrawn
{'balance': 700}

This is the same idea databases use for SQL transactions: all or nothing.

Key takeaway#

Finished this lesson?