Frontend Labs
⋯

8. Files, paths, and context managers

8.1 with: deterministic cleanup

with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("first line\n")
# f is closed here, even if an exception was raised inside the block

with open("notes.txt", encoding="utf-8") as f:
    for line in f:                 # streams line by line; constant memory
        print(line.rstrip("\n"))

with calls __enter__ at the start and __exit__ at the end, however the block exits. It is Python's equivalent of Rust's Drop and Go's defer, but explicit. Use it for every file, socket, lock, database transaction, and temporary directory.

Always pass encoding="utf-8" for text files. Without it Python uses the platform's locale encoding, which differs between your Mac and a Windows or minimal Docker machine. (3.15 makes UTF-8 the default; until then, be explicit.)

Mode Meaning
"r" / "w" / "a" read / write (truncate!) / append, text
"rb" / "wb" binary: you get bytes
"x" create, fail if it exists (no accidental overwrite)
"r+" read and write

8.2 pathlib: paths as objects

from pathlib import Path

root = Path.home() / "projects"            # / joins paths, on any OS
cfg = Path("config") / "app.toml"
print(cfg.suffix, cfg.stem, cfg.name, cfg.parent)   # .toml app app.toml config

data = Path("data")
data.mkdir(parents=True, exist_ok=True)
(data / "hello.txt").write_text("hi\n", encoding="utf-8")
print((data / "hello.txt").read_text(encoding="utf-8"))
for p in sorted(data.glob("*.txt")):        # rglob for recursive
    print(p, p.stat().st_size, "bytes")

Prefer pathlib over string paths and os.path. It is clearer and avoids the separator bugs between Windows and POSIX.

8.3 Writing your own context manager

The easiest way is a generator with contextlib.contextmanager:

import time
from contextlib import contextmanager

@contextmanager
def timed(label: str):
    start = time.perf_counter()
    try:
        yield                       # the body of the `with` block runs here
    finally:
        print(f"{label}: {(time.perf_counter() - start) * 1000:.1f} ms")

with timed("sum"):
    total = sum(range(1_000_000))

contextlib also has suppress, chdir (3.11+), and ExitStack for a dynamic number of resources:

from contextlib import ExitStack, suppress
from pathlib import Path

with suppress(FileNotFoundError):
    Path("maybe.tmp").unlink()

paths = [Path("data/hello.txt")]
with ExitStack() as stack:
    files = [stack.enter_context(open(p, encoding="utf-8")) for p in paths]
    print([f.readline().strip() for f in files])

8.4 Structured formats in the standard library

import csv, json, tomllib
from pathlib import Path

Path("users.json").write_text(json.dumps([{"name": "ada", "admin": True}], indent=2))
users = json.loads(Path("users.json").read_text())
print(users[0]["admin"])

with open("users.csv", "w", newline="", encoding="utf-8") as f:   # newline="" for csv
    w = csv.DictWriter(f, fieldnames=["name", "admin"])
    w.writeheader()
    w.writerows(users)

Path("app.toml").write_text('[server]\nport = 8080\nhosts = ["a", "b"]\n')
with open("app.toml", "rb") as f:               # tomllib needs binary mode
    cfg = tomllib.load(f)
print(cfg["server"]["port"])

Security note: never use pickle or yaml.load on data you did not create. Both can execute code. Chapter 47 shows the exploit.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36