Step 08 of 10

Files & modules

Read and write text, CSV and JSON files, use Python's standard library, and split your own programs into modules.

17 min 9 sections

Until now, your programs have forgotten everything the moment they finished, and every program has been a single file. Real programs read data from files and save results, and they're split into several files that each do one job, reusing code other people have written. This lesson covers reading and writing text, CSV and JSON files, importing from Python's standard library, and organising your own code into modules.

Reading a text file

Create a file called todo.txt next to your program, with a few lines:

text
buy milk
call the bank
finish the Python lesson

Read it with open:

python
with open("todo.txt", encoding="utf-8") as f:
    for line in f:
        print(f"- {line.strip()}")
text
- buy milk
- call the bank
- finish the Python lesson

What each part does:

  • open("todo.txt") opens the file and gives you a file object, here named f.
  • with ... as f: makes sure the file is closed when the block ends, even if an error happens inside it. Always open files with with.
  • encoding="utf-8" says how the bytes in the file map to characters. Without it, Python uses your operating system's default, which on some Windows setups garbles accented letters and emoji. Always pass it for text files.
  • Looping over f gives one line at a time, including the newline character at the end, which is why .strip() is there.

To get the whole file as one string, use f.read(); to get a list of lines, f.read().splitlines().

Where is the file?

A relative path like "todo.txt" is relative to the folder you ran the program from (your terminal's current directory), not the folder the .py file is in. If you get FileNotFoundError, check where you are with pwd.

The pathlib module gives you a clearer way to work with paths:

python
from pathlib import Path

todo_path = Path("todo.txt")
if todo_path.exists():
    lines = todo_path.read_text(encoding="utf-8").splitlines()
    print(f"{len(lines)} tasks")
else:
    print("No todo list yet.")

Path("data") / "2026" / "sales.csv" builds paths with /, and produces the right separator on every operating system.

Writing files

Open with mode "w" to write (this replaces any existing contents) or "a" to append to the end:

python
tasks = ["buy milk", "call the bank"]

with open("todo.txt", "w", encoding="utf-8") as f:
    for task in tasks:
        f.write(task + "\n")        # write() doesn't add newlines for you

with open("log.txt", "a", encoding="utf-8") as f:
    f.write("Program ran\n")         # adds a line without erasing earlier ones

CSV: spreadsheet data

Comma-separated values (CSV) is the plain-text format spreadsheets export. Save this as expenses.csv:

text
date,category,amount
2026-10-01,food,12.40
2026-10-01,transport,2.80
2026-10-02,food,8.15
2026-10-03,rent,750.00

Splitting lines on commas yourself breaks as soon as a value contains a comma ("Smith, Ana"). Use the csv module from Python's standard library, which handles quoting properly. csv.DictReader turns each row into a dictionary keyed by the header row:

python
import csv

with open("expenses.csv", newline="", encoding="utf-8") as f:
    rows = list(csv.DictReader(f))

print(rows[0])          # {'date': '2026-10-01', 'category': 'food', 'amount': '12.40'}

total = sum(float(row["amount"]) for row in rows)
print(f"Total spent: ${total:.2f}")      # Total spent: $773.35

Notice that every value comes back as a string, even '12.40'. Files contain text; converting to numbers is your job. (newline="" is what the csv module asks for when opening files, so it can handle line endings itself.)

Writing works the same way, with csv.DictWriter:

python
with open("summary.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["category", "total"])
    writer.writeheader()
    writer.writerow({"category": "food", "total": 20.55})

JSON: structured data

JSON stores nested lists and dictionaries as text. It's the format of almost every web API and many configuration files, and it maps directly onto Python's collections:

python
import json

settings = {"theme": "dark", "font_size": 14, "recent": ["notes.txt", "todo.txt"]}

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)

print(loaded["recent"][0])     # notes.txt

settings.json now contains:

json
{
  "theme": "dark",
  "font_size": 14,
  "recent": [
    "notes.txt",
    "todo.txt"
  ]
}

Unlike CSV, JSON keeps numbers as numbers and booleans as booleans. json.dumps(value) and json.loads(text) (with an s for "string") do the same with strings instead of files.

Handling missing and broken files

Files are outside your program's control, so expect problems:

python
import json

def load_settings(path):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return {"theme": "light", "font_size": 12}     # first run: use defaults

A missing settings file on the first run is normal, so returning defaults is the right response. A corrupted settings file (json.JSONDecodeError) is different: you probably want to tell the user rather than silently overwrite it.

Modules: using code that already exists

A module is a file of Python code you can import. Python ships with a large standard library of modules, so a lot of what you need already exists:

python
import math
import random
from datetime import date

print(math.sqrt(2))                       # 1.4142135623730951
print(random.choice(["rock", "paper", "scissors"]))
print(date.today().isoformat())           # e.g. 2026-10-31

There are two import styles. import math gives you the module, and you write math.sqrt. from datetime import date brings one name in directly. Both are fine; the first makes it clearer where a name came from.

A few standard modules worth knowing: pathlib (paths), csv and json (data formats), datetime (dates and times), random, math, statistics (mean, median), collections (Counter counts things in one line), and os and sys (the operating system and the interpreter).

Thousands more packages exist outside the standard library, installed with a tool called pip. The Python roadmap covers installing packages and managing projects properly.

Splitting your own program into modules

Any .py file is a module. Once a program grows, move related functions into their own file:

python
# money.py
def format_money(amount):
    """Return an amount as a string like $1,234.50."""
    return f"${amount:,.2f}"


def parse_money(text):
    """Turn '1,234.50' or '$12' into a float."""
    return float(text.replace("$", "").replace(",", ""))
python
# report.py
from money import format_money, parse_money

print(format_money(parse_money("$1,234.5")))   # $1,234.50

Run python3 report.py from the folder that contains both files, and Python finds money.py there.

When Python imports a module, it runs the whole file. So if money.py had test print calls at the bottom, they'd run on every import. Guard code that should only run when the file is executed directly:

python
# at the bottom of money.py
if __name__ == "__main__":
    print(format_money(1234.5))   # runs for python3 money.py, not for import money

Python sets __name__ to "__main__" for the file you ran, and to the module's name for files that are imported.

Practice

Using the expenses.csv file from this lesson (add a few more rows of your own):

  1. Write expenses.py that reads the CSV and calculates the total per category with a dictionary.
  2. Print the categories sorted from biggest to smallest total, with amounts formatted as money.
  3. Save the totals to summary.json.
  4. Move the money formatting into a money.py module and import it.
  5. Make the program print a friendly message instead of crashing if expenses.csv doesn't exist.
Show solution
python
# money.py
def format_money(amount):
    """Return an amount as a string like $1,234.50."""
    return f"${amount:,.2f}"
python
# expenses.py
import csv
import json
import sys

from money import format_money


def load_expenses(path):
    with open(path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))


def totals_by_category(rows):
    totals = {}
    for row in rows:
        category = row["category"]
        totals[category] = totals.get(category, 0) + float(row["amount"])
    return totals


def main():
    try:
        rows = load_expenses("expenses.csv")
    except FileNotFoundError:
        print("Couldn't find expenses.csv in this folder. Create it first.")
        sys.exit(1)

    totals = totals_by_category(rows)
    for category, amount in sorted(totals.items(), key=lambda pair: pair[1], reverse=True):
        print(f"{category:<12} {format_money(amount):>12}")

    with open("summary.json", "w", encoding="utf-8") as f:
        json.dump({k: round(v, 2) for k, v in totals.items()}, f, indent=2)
    print("Saved summary.json")


if __name__ == "__main__":
    main()
text
rent              $750.00
food               $20.55
transport           $2.80
Saved summary.json

sys.exit(1) stops the program with exit code 1, the conventional signal to the shell that something went wrong. round(v, 2) keeps floating-point noise such as 20.549999999999997 out of the JSON file. The {k: ... for k, v in ...} expression is a dictionary comprehension, the dictionary version of the list comprehension you saw earlier. Putting the work in main() and calling it under if __name__ == "__main__": means another program could import totals_by_category without triggering the whole report.

Key takeaways

  • Open files with with open(path, encoding="utf-8") so they're always closed; relative paths start from where you ran the program.
  • Mode "w" replaces a file's contents, "a" appends; write doesn't add newlines.
  • Use the csv module for spreadsheet data (values arrive as strings) and json for nested data.
  • Expect missing or broken files and handle FileNotFoundError where there's a sensible fallback.
  • Import from the standard library before writing your own, split programs into modules, and guard script-only code with if __name__ == "__main__":.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.Why open files with a with statement?

  2. 2.csv.DictReader reads a row with amount 12.40. What type is row["amount"]?

  3. 3.You open an existing notes.txt with mode "w" and the program crashes before writing anything. What happened to the file?

  4. 4.What does the if __name__ == "__main__": block at the bottom of a module do?

Finished reading?

Mark it complete to fill in your roadmap.