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.