Context managers and why you should always use with
You have been writing with without being told what it does. It is a small
feature with a specific job, and module 6 deferred to it when discussing
finally.
What it replaces
Without with, opening a file properly looks like this:
file = open("notes.txt", encoding="utf-8")
try:
contents = file.read()
finally:
file.close()
The finally is not optional. Without it, an error while reading leaves the
file open — and finally is the only construct that runs whether the block
succeeds, fails, or returns early.
with is that, in one line:
with open("notes.txt", encoding="utf-8") as file:
contents = file.read()
Same guarantee. The file is closed when the block ends, however it ends —
normally, via an exception, or through a return in the middle.
Why leaving it open matters
"It closes eventually" is roughly true and not good enough.
Buffered writes may not have happened. Writing does not go straight to disk; it sits in a buffer until there is enough to be worth writing. Closing flushes that buffer. A program that writes without closing can exit with the file empty or truncated — and worse, it usually works on small files and fails on large ones, which is the kind of bug that reaches production.
file = open("out.txt", "w", encoding="utf-8")
file.write("important data")
# no close, and the program crashes here
That file may well be empty.
Operating systems limit open files. A few thousand per process. A loop
opening files without closing them hits OSError: Too many open files after a
while — far from the code that caused it.
Windows locks open files. Another process cannot delete or rename a file you still hold open, which produces confusing failures on somebody else's machine.
CPython does close files when the object is garbage collected, which is why sloppy code often seems fine. That is an implementation detail, not a promise, and it does not hold on other Python implementations. Do not rely on it.
How it works
Any object with two specific methods can be used with with:
__enter__()— runs on entry; whatever it returns is bound byas__exit__()— runs on exit, guaranteed
Those double-underscore names are "dunder" methods, covered properly in module
9. For now: with is not special syntax for files. It is a general mechanism,
and files happen to support it.
That is why as file gives you the file object — open() returns something
whose __enter__ hands back itself.
Several at once
with open("input.txt", encoding="utf-8") as source, \
open("output.txt", "w", encoding="utf-8") as target:
for line in source:
target.write(line.upper())
Both close correctly, in reverse order. The backslash continues the line; on Python 3.10 and later you can bracket the whole group instead:
with (
open("input.txt", encoding="utf-8") as source,
open("output.txt", "w", encoding="utf-8") as target,
):
...
Copying a file while transforming it, without ever holding it all in memory.
Where else you will meet it
with is not just for files. It appears anywhere something must be released:
with sqlite3.connect("data.db") as connection:
...
with requests.Session() as session:
...
with lock:
...
The pattern is always "acquire something, guarantee it is given back". When you
see with in unfamiliar code, that is what it means.
Writing your own
You will not need this often, and it is worth seeing once so the machinery is not mysterious:
import time
from contextlib import contextmanager
@contextmanager
def timed(label: str):
"""Print how long the enclosed block took."""
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.3f}s")
with timed("loading data"):
total = sum(range(10_000_000))
loading data: 0.284s
Everything before yield runs on entry, everything after runs on exit, and the
finally means it runs even if the block raises. The @contextmanager line is
a decorator — module 9 territory — and you can use this pattern now without
understanding it fully.
That timing block is genuinely useful when you are working out which part of a program is slow.
Does it swallow errors?
No, and this catches people:
with open("notes.txt", encoding="utf-8") as file:
value = int(file.read())
If the file contains abc, ValueError is raised and propagates normally. The
file is closed on the way out, but with does not handle the exception.
To handle it, combine them:
try:
with open("notes.txt", encoding="utf-8") as file:
value = int(file.read())
except (FileNotFoundError, ValueError):
value = 0
with manages the resource. try/except handles errors. Different jobs,
frequently used together.
Practice
- Rewrite a
withblock asopen/try/finallyand confirm both work. - Write to a file without closing it and without
with, then crash the program deliberately before it ends. Check whether the file has your data. - Write a
withblock that raises an exception partway through. Confirm the file still closed by opening it again immediately. - Copy a file while uppercasing it, using two files in one
with. - Build the
timedcontext manager and use it to time two different ways of summing a million numbers. - Put a
returninside a function'swithblock and confirm the file still closes. - Explain in one sentence why
withdoes not remove the need fortry/except.
Next: JSON — the format almost every API and config file uses.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship