Expert · Advanced Python · topic 42 of 50 · 30 min
Context managers and the with statement
Explanation
The problem: forgetting to clean up
Many things must be closed or undone after use: a file, a network connection, a lock that stops two threads from clashing, a setting you changed for a moment. If an error happens in the middle, the code that closes it may never run. Over time, open files and connections pile up and the program slows down or crashes.
The usual fix is try/finally, because a finally block always runs. A context manager packs that try/finally into a reusable object, and the with statement uses it.
The with statement
with open("marks.csv") as f: # the pattern you know from files
data = f.read()
# ...is roughly the same as:
f = open("marks.csv")
try:
data = f.read()
finally:
f.close() # runs even if read() fails__enter__ and __exit__
Any object with two dunder methods is a context manager. __enter__ runs at the start; whatever it returns is given to the name after "as". __exit__ runs at the end and receives three values about any exception (its type, the exception itself and the traceback), or three Nones if all went well.
class FakeConnection:
def __init__(self, log):
self.log = log
def __enter__(self):
self.log.append("open")
return self # this becomes "conn" below
def __exit__(self, exc_type, exc, tb):
self.log.append("close")
return False # False: do not hide errors
def send(self, text):
self.log.append("send " + text)
log = []
try:
with FakeConnection(log) as conn:
conn.send("hello")
raise ValueError("network dropped")
except ValueError:
pass
print(log) # ['open', 'send hello', 'close']The easy way: contextlib.contextmanager
Writing a class is a lot of typing. The contextmanager decorator from the contextlib module turns a generator function into a context manager. Code before yield is the set-up, the yielded value goes to "as", and code after yield is the clean-up. Put the yield inside try/finally so the clean-up runs even on errors.
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
seconds = time.perf_counter() - start
print(label, "took", round(seconds * 1000, 2), "ms")
with timer("adding numbers"):
total = sum(range(100_000))Changing a setting for a moment
from contextlib import contextmanager
settings = {"language": "en"}
@contextmanager
def temporary(key, value):
old = settings[key]
settings[key] = value
try:
yield
finally:
settings[key] = old # always put it back
with temporary("language", "hi"):
print(settings["language"]) # hi
print(settings["language"]) # enIgnoring one expected error with contextlib.suppress
contextlib.suppress(SomeError) is a ready-made context manager that silently ignores that one kind of exception inside the block. It is a short, clear replacement for try/except/pass.
from contextlib import suppress
stock = {"rice": 10}
with suppress(KeyError):
del stock["sugar"] # no sugar key: KeyError is ignored
print(stock)| Need | Tool |
|---|---|
| Open and close a file safely | with open(...) as f |
| Your own set-up and clean-up, quickly | @contextlib.contextmanager with one yield |
| Your own context manager with state and methods | A class with __enter__ and __exit__ |
| Ignore one expected exception | with contextlib.suppress(ErrorType) |
Watch the lesson
Context managers and the with statement · 1/6
In this lesson you will learn the with statement and how to write your own context managers.
Prefer a video by a teacher? Find free videos on “python context managers with statement contextlib tutorial” on YouTube ↗
In real life: A UPI payment that always releases its lock
When a payment app debits a bank account, it first locks that account so two payments cannot spend the same money at the same moment. If the code crashes after locking but before unlocking, the account stays stuck. Wrapping the work in a context manager guarantees the lock is released, whatever happens.
from contextlib import contextmanager
locked = set()
@contextmanager
def account_lock(account):
locked.add(account)
try:
yield
finally:
locked.discard(account)
try:
with account_lock("ACC-001"):
print("locked:", locked)
raise RuntimeError("bank server did not reply")
except RuntimeError as err:
print("payment failed:", err)
print("locked after:", locked)Practice (0/3 done)
Write a class Recorder that is a context manager. Recorder(log) receives a list. __enter__ appends "open" to the list and __exit__ appends "close". __exit__ must not hide errors (return False).
Using contextlib.contextmanager, write temp_setting(settings, key, value). Inside the with block, settings[key] must equal value. After the block, the old value must be back, even if an error happened inside.
Write safe_int(text) that returns int(text), or None when the text is not a whole number. Use contextlib.suppress(ValueError) instead of try/except.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.