The Quiet Terminal

← Beginner

Reading and writing files, the with-block way

2026-06-04 · 3 min read

Files are how a script remembers anything after it exits. My first file-handling code skipped the with block, skipped the encoding, and once lost a log I actually needed. This post is the version I'd hand my past self: the modes that cover real life, and why with is not optional.

Writing and appending

with open("notes.txt", "w", encoding="utf-8") as f:    # write from scratch
    f.write("bought coffee beans\n")
    f.write("call the dentist\n")

with open("notes.txt", "a", encoding="utf-8") as f:    # append to the end
    f.write("renew library card\n")

Three things worth noticing. The mode is the second argument: "w" writes from scratch, "a" adds to the end. Every write() needs its own \n, because write() adds nothing on its own. And encoding="utf-8" is there on purpose — hold that thought for the end.

The mode that cost me a log file: "w" empties the file immediately, before a single byte of new data is written. If the script then crashes halfway through, you're left with a truncated file and good intentions. Anything that accumulates over days — a log, a notes file — belongs to "a".

Why the with block matters

with guarantees the file gets closed when the block ends — including when an error happens inside it. The manual version has no such promise:

f = open("notes.txt", "w", encoding="utf-8")
f.write("half a line")
f.close()     # never runs if write() raises

One exception between open and close, and the handle leaks — on Windows the file can even stay locked against other programs. And there's a subtler loss: writes sit in a buffer, and close() is what flushes them to disk. My lost log wasn't deleted; it was still sitting in memory when the script crashed, and the flush never happened. The file existed and was empty — a bug with no obvious suspect.

Reading back

with open("notes.txt", encoding="utf-8") as f:
    contents = f.read()          # the whole file as one string
# 'bought coffee beans\ncall the dentist\nrenew library card\n'

with open("notes.txt", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        print(i, line.rstrip())
# 1 bought coffee beans
# 2 call the dentist
# 3 renew library card

read() is right for small files. For anything that might be big, loop instead — Python streams the lines one at a time and never holds the whole file in memory. Each line still carries its trailing \n, which is what rstrip() is for: without it, print's own newline doubles the gap between lines.

A tiny notes script

Put together — append to write, loop to read — this is a shape I keep on every machine:

def add_task(text):
    with open("todo.txt", "a", encoding="utf-8") as f:
        f.write(text + "\n")

def show_tasks():
    with open("todo.txt", encoding="utf-8") as f:
        for line in f:
            print("-", line.rstrip())

add_task("water the plants")
add_task("back up the blog")
show_tasks()

# - water the plants
# - back up the blog

Append mode even creates the file if it doesn't exist yet, so the first task of the day just works.

The one habit to steal

Always pass encoding="utf-8". Without it, Python uses your operating system's default, and those defaults differ between machines — my Windows laptop and the little Linux box that runs my backups once disagreed about a notes file, and every apostrophe in it lost. UTF-8 everywhere turned the problem from occasional to impossible. For small files, my post on pathlib shows the one-line shortcuts, read_text() and write_text() — the same advice about encoding applies there.