Python105 min total · 18 parts
Python Fundamentals for Interviews: Data Structures, Comprehensions, and Gotchas
Part 11 of 18 · ~2 min
Context Managers and with
with open("gateway.log") as f:
first_line = f.readline()
# f.close() runs no matter what — even if .readline() throws partway through
Nothing about with is magic underneath — it's a try/finally wearing a shorter costume, purpose-built for the single most common shape that pattern takes: open something, do work with it, and make absolutely sure it gets closed regardless of whether the work in between succeeded. Building a class that plugs into that shape takes exactly two methods:
class IngestWindow:
def __enter__(self):
self.start = time.perf_counter()
return self # becomes the "as x" value
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.perf_counter() - self.start
print(f"Ingest finished in {elapsed:.4f}s")
return False # don't suppress whatever exception happened
with IngestWindow():
report = Report.from_file("gateway.log")
# "Ingest finished in 0.0421s"
Those three parameters on __exit__ only carry real values when the with block raised something; otherwise all three arrive as None. What __exit__ returns then decides what happens to that exception next — a truthy return tells Python the exception has been handled and should go no further, which is occasionally the right call and, far more often, an exception someone genuinely needed to see getting quietly buried. There's a shorter way to write a context manager that sidesteps having to remember any of this, using a generator function and a single yield:
from contextlib import contextmanager
@contextmanager
def ingest_window():
start = time.perf_counter()
yield # the "with" block's code runs here
print(f"Ingest finished in {time.perf_counter() - start:.4f}s")
Whatever runs before yield takes on __enter__'s job; whatever runs after it takes on __exit__'s — the single pause point is doing double duty as the boundary between them.
A with statement can also hold more than one context manager at once, entered left to right and exited in reverse — useful the moment the watcher needs to compare two batches side by side:
with open("morning.log") as fa, open("afternoon.log") as fb:
... # both are guaranteed closed on the way out, even if the block raises partway through
That's equivalent to nesting two separate with blocks, just without the extra indentation.