# Files & modules

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

Learn to Code roadmap · Writing real programs · 17 min read · https://levelcode.org/roadmaps/learn-to-code/files-and-modules

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
```

> [!WARNING]
> Opening an existing file with `"w"` empties it immediately, before you write anything. If your program crashes halfway, the old contents are gone. For important files, write to a new file first and rename it over the old one when you're done.

## 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](https://levelcode.org/roadmaps/python) 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.

**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__":`.

## Quiz

1. Why open files with a with statement?

   A. It makes reading faster
   B. It closes the file automatically when the block ends, even if an error occurs
   C. It's the only way to read CSV files
   D. It creates the file if it doesn't exist

   **Answer: B.** with guarantees the file is closed, which matters for writes being saved and for not leaking open files. It doesn't change whether missing files are created.

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

   A. float
   B. int
   C. Decimal
   D. str

   **Answer: D.** CSV is plain text and the csv module doesn't guess types, so every value is a string. Convert with float() or int().

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

   A. It's unchanged
   B. It was emptied when it was opened
   C. Python restores it automatically
   D. It was renamed

   **Answer: B.** Opening with "w" truncates the file immediately. Write to a temporary file and rename it when you need to protect existing contents.

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

   A. Runs its code only when the file is executed directly, not when it's imported
   B. Makes the module private
   C. Runs the code every time the module is imported
   D. Is required for every Python file

   **Answer: A.** Python sets __name__ to "__main__" only for the file you ran, so the guarded code doesn't run when another file imports the module.
