RizTech Academy logo
RizTech Academy
Files and DataLesson 2 of 515 min

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 by as
  • __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

  1. Rewrite a with block as open/try/finally and confirm both work.
  2. 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.
  3. Write a with block that raises an exception partway through. Confirm the file still closed by opening it again immediately.
  4. Copy a file while uppercasing it, using two files in one with.
  5. Build the timed context manager and use it to time two different ways of summing a million numbers.
  6. Put a return inside a function's with block and confirm the file still closes.
  7. Explain in one sentence why with does not remove the need for try/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