Python Book · Chapter 9

Working with files

Variables disappear when your program ends. Files are how programs remember things, share data and read the world's spreadsheets and settings.

Writing a file

with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("First line\n")
    f.write("Second line\n")

open takes a file name and a mode: "r" to read (the default), "w" to write (this replaces anything already in the file), and "a" to append to the end. Unlike print, write does not add a new line for you, so include \n.

The with block closes the file automatically when it ends, even if an error happens inside it. Always use it.

Always pass encoding="utf-8" too. Otherwise Python uses your system's default, which differs between Windows, macOS and Linux and can garble accented letters.

Reading a file

with open("notes.txt", encoding="utf-8") as f:
    text = f.read()          # the whole file as one string

with open("notes.txt", encoding="utf-8") as f:
    for line in f:           # one line at a time
        print(line.rstrip())

Each line keeps its \n at the end, so rstrip() removes it before printing. Looping over the file reads one line at a time, which works even for files too big to fit in memory.

Appending

with open("log.txt", "a", encoding="utf-8") as f:
    f.write("Program started\n")

Run that three times and the file has three lines.

Paths with pathlib

File paths use \ on Windows and / elsewhere. pathlib handles the difference for you:

from pathlib import Path

data = Path("data")
data.mkdir(exist_ok=True)           # create the folder if needed
scores = data / "scores.txt"        # joins paths correctly on any system

scores.write_text("88\n95\n91\n", encoding="utf-8")
print(scores.exists())              # True
print(scores.read_text(encoding="utf-8").split())   # ['88', '95', '91']

for path in data.glob("*.txt"):     # every .txt file in the folder
    print(path.name)

Relative paths like "data" are relative to the folder you ran Python from, not the folder the script is in. If a file "cannot be found", check that first.

CSV files

Spreadsheets are often shared as CSV (comma-separated values). Use the csv module instead of splitting on commas yourself, because values can contain commas inside quotes.

import csv

with open("grades.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "grade"])
    writer.writerow(["Ada", 91])
    writer.writerow(["Linus", 78])

with open("grades.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        print(row["name"], int(row["grade"]))

DictReader uses the first row as column names. Everything comes back as strings, so convert numbers with int or float. The newline="" argument is what the csv documentation asks for, and avoids blank lines between rows on Windows.

JSON files

JSON is the most common format for structured data and settings. It maps neatly onto Python dictionaries and lists:

import json

settings = {"theme": "dark", "font_size": 14, "recent": ["a.py", "b.py"]}

with open("settings.json", "w", encoding="utf-8") as f:
    json.dump(settings, f, indent=2)

with open("settings.json", encoding="utf-8") as f:
    loaded = json.load(f)

loaded["recent"][0]   # 'a.py'

json.dumps and json.loads (with an s, for "string") do the same with strings instead of files.

Exercises

1. Line count

Write count_lines(path) that returns how many lines a text file has.

Show answer
def count_lines(path):
    with open(path, encoding="utf-8") as f:
        return sum(1 for _ in f)

This reads one line at a time, so it works on huge files. Returning from inside with still closes the file.

2. Class average from a CSV

Using the grades.csv file from this chapter, print the average grade.

Show answer
import csv

with open("grades.csv", newline="", encoding="utf-8") as f:
    grades = [int(row["grade"]) for row in csv.DictReader(f)]

print(sum(grades) / len(grades))   # 84.5

3. A tiny to-do list that remembers

Write a program that loads a list of tasks from todo.json (or starts with an empty list if the file does not exist), asks the user for a new task, adds it and saves the list again.

Show answer
import json
from pathlib import Path

path = Path("todo.json")
tasks = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []

tasks.append(input("New task: "))
path.write_text(json.dumps(tasks, indent=2), encoding="utf-8")

for i, task in enumerate(tasks, start=1):
    print(f"{i}. {task}")