Course outline · 0% complete

0/27 lessons0%

Course overview →

pathlib and context managers

lesson 8-1 · ~12 min · 21/27

A reminder from lesson 8-0: a file object is an iterator over its lines. Looping over the same open file object a second time therefore produces nothing at all, because the iterator has already been exhausted.

This is the one-shot rule from unit 4 showing up in file handling. An open file remembers a position, and once that position reaches the end there is nothing left to hand out. Reopening the file, or calling f.seek(0) to rewind it, gives you another pass. Keep that in mind through this lesson's examples.

Paths as objects

In lesson 8-0 you opened files by handing open() a plain string path. Strings work, but they carry no behavior. Gluing folders together with + risks the wrong separator on another operating system, since Windows uses \ while macOS and Linux use /, and a check like "does this file exist" has no obvious place to live.

The modern tool is pathlib.Path, which turns paths into objects with methods:

from pathlib import Path

p = Path("notes.txt")
p.write_text("line one\nline two\n")
print(p.read_text())
print(p.exists(), p.suffix)   # True .txt

Joining paths uses the / operator, which produces the correct separator on every operating system:

data_file = Path("data") / "users" / "ada.json"

For quick whole-file reads and writes, read_text and write_text are all you need, and they open and close the file for you. For anything large enough that you would rather not hold it all in memory at once, keep using open and stream it line by line.

with: the context manager

When you process a file line by line, or append, you still call open. The rule is to always do it inside a with block:

with open("notes.txt") as f:
    for line in f:
        print(line.strip())

with runs a context manager: it guarantees the file is closed when the block ends, even if an exception happens inside. Forgetting to close files leaks resources and can lose buffered writes. The same with pattern manages database connections and locks later in your career, so make it a reflex now: open never appears without with.

Streaming a file with a numbered loop

pathlib writes the small file in one call, and a with block streams it back line by line.

from pathlib import Path

p = Path("shopping.txt")
p.write_text("milk\nbread\neggs\n")

with open(p) as f:
    for i, line in enumerate(f, start=1):
        print(i, line.strip())

print("closed:", f.closed)

Output

1 milk
2 bread
3 eggs
closed: True

open accepts a Path object directly, so there is no need to convert it back to a string. enumerate(f, start=1) pairs each line with a human-friendly line number, which beats maintaining your own counter variable.

The final line is the proof of the whole idea. After the with block ends, f.closed is already True. Nobody called close(), and nobody had to.

Writing and totaling scores

This combines both halves of the lesson: write_text creates the file in one call, and a with block reads it back and sums the numbers.

from pathlib import Path

scores = [82, 91, 78]

p = Path("scores.txt")
p.write_text("\n".join(str(s) for s in scores) + "\n")

total = 0
with open(p) as f:
    for line in f:
        total += int(line)
print(total)

Output

251

"\n".join(str(s) for s in scores) builds the whole file body in one expression, feeding a generator expression from lesson 4-2 straight into join. That produces "82\n91\n78", and the trailing + "\n" gives the last line its newline too, which is the convention every text tool expects.

Coming back the other way, lines read from a file are strings, so int(line) does the conversion, and the trailing newline is no obstacle.

The guarantee a with block makes

with open("a.txt") as f: guarantees that f is closed when the block exits, including when the block exits because of an exception.

That last clause is the whole reason the idiom exists. A manual f.close() on the line after the work is skipped entirely if anything above it raises, and the file stays open with buffered data possibly unwritten. A context manager's cleanup runs on every exit path, normal or not.

Every file you open in production code should be opened with with. The manual open/close pair from lesson 8-0 was there to show you the moving parts, not as a style to keep.