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}")