Day 10 / 25 Files, Modules & Mini-Project 2 0/0 exercises Exercises ↓

AmouAI Hub/Courses/Programming Fundamentals/Day 10

Week 2 · Functions & Data Structures · Day 10

Files, Modules & Mini-Project 2

Programs that outlive their own run: reading and writing files, and splitting code across modules.

Study time
4 hours
Reading
Think Python, Ch. 14
Focus
with · imports · Mini-Project 2

By the end of today you can

  1. Read and write text files, and say why with is not optional
  2. Choose the right mode — r, w, a — and know what w destroys
  3. Handle a missing file deliberately rather than crashing
  4. Read and write CSV without hand-rolling a parser
  5. Split a program across modules and import between them
  6. Explain what if __name__ == "__main__": actually does
  7. Find and use the right standard-library module
  8. Deliver Mini-Project 2

Today's videos

Watch each video, then work the matching sections below. Watching alone will not do it.

10A
Files, and Why `with` Is Not Optional (120 min)
Reading and writing text · the four modes and what w destroys · why encoding is not optional · handling a missing file deliberately · CSV with DictReader, and why you must not write your own parser.
10B
Modules & Mini-Project 2 (120 min)
Splitting a program across files · import styles and why import * is banned · what if __name__ == "__main__" actually does · a tour of the standard library · the full Contact Book build.

1with, and why

Every program you have written so far forgot everything the moment it ended.

files.py
# writing
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("first line\n")
    f.write("second line\n")

# reading, whole file
with open("notes.txt", encoding="utf-8") as f:
    content = f.read()

# reading, line by line — the memory-safe way
with open("notes.txt", encoding="utf-8") as f:
    for line in f:
        print(line.rstrip("\n"))
first line second line
What with is actually doing

It guarantees the file is closed — even if an exception is raised inside the block. Without it, a crash halfway through leaves the file open and the last writes possibly unflushed. with is not a style preference; it is the difference between reliable and nearly reliable.

ModeDoesCareful
rRead. The default.FileNotFoundError if it does not exist
wWriteTruncates an existing file to zero bytes immediately
aAppendCreates the file if needed; never destroys
xCreateFails if the file already exists — useful as a guard
w deletes before it writes

Opening an existing file with "w" empties it the instant open() returns — before you have written a single byte. If your program then crashes, the original is gone. When in doubt use "a", or write to a temporary file and rename.

Always pass encoding

encoding="utf-8" is not optional in practice. Without it Python uses whatever the operating system prefers, so a file that reads perfectly on your machine raises UnicodeDecodeError on someone else's. State it every time.

Missing files

missing.py
import os

# ask first
if os.path.exists("config.txt"):
    with open("config.txt", encoding="utf-8") as f:
        settings = f.read()
else:
    settings = ""

# or try, and handle the failure — this is Day 15's territory
try:
    with open("config.txt", encoding="utf-8") as f:
        settings = f.read()
except FileNotFoundError:
    settings = ""

2Structured files: CSV

Do not write your own parser. Really.

A comma-separated file looks trivial to parse with .split(",") — and it is, until one field contains a comma, or a quote, or a newline. All three happen in real data. The standard library already solved this.

csv_read.py
import csv

with open("scores.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["name"], row["score"])
csv_write.py
import csv

rows = [{"name": "Amin", "score": 88}, {"name": "Ada", "score": 95}]

with open("out.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "score"])
    writer.writeheader()
    writer.writerows(rows)
newline="" is required, not decorative

The csv module does its own line-ending handling. Omit newline="" and on Windows you get a blank line between every row. It is in the documentation and everybody skips it once.

Note that everything DictReader gives you is a stringrow["score"] is "88", not 88. Converting, and deciding what to do when the conversion fails, is your job. That is Mini-Project 2's real content.

3Modules

One file stops being enough at about 300 lines.

A module is just a .py file. Importing it runs it once and gives you its names.

Import styleUse when
import statsDefault. The stats. prefix says where the name came from.
from stats import mean_ofYou use it constantly and the name is unambiguous.
from stats import *Never. It hides where names came from and shadows silently.

The line everybody copies without understanding

main_guard.py
def main():
    print("running as a program")


if __name__ == "__main__":
    main()

Python sets __name__ to "__main__" in the file you ran, and to the module's own name in anything you imported. So the guard means: run this only when I am the program, not when I am being imported.

Why it matters the moment you have two files

Without the guard, import stats would execute everything at the bottom of stats.py — prompting for input, printing a report, overwriting a file. The guard is what lets one file be both a runnable program and an importable library.

4The standard library

Most of what you are about to write already exists.

ModuleForOne thing worth knowing
pathlibPaths and filesPath("a")/"b" beats string concatenation everywhere
csvTabular textDictReader gives you named columns
jsonStructured datajson.dump/load — the format every API speaks
collectionsBetter containersCounter, defaultdict — yesterday's patterns, built in
datetimeDates and timesNever do date arithmetic by hand
randomRandomnessseed() makes a random program reproducible for tests
mathMathsisclose() is how you compare floats
os / sysThe environmentsys.argv for command-line arguments
stdlib.py
from collections import Counter
from pathlib import Path
import json

# yesterday's counting pattern, in one line
print(Counter("abracadabra").most_common(3))

# paths that work on every operating system
p = Path("data") / "scores.csv"
print(p.suffix, p.stem, p.exists())

# structured data, saved and reloaded
record = {"name": "Amin", "scores": [88, 95]}
text = json.dumps(record)
print(text)
print(json.loads(text)["scores"])
[('a', 5), ('b', 2), ('r', 2)] .csv scores False {"name": "Amin", "scores": [88, 95]} [88, 95]
The habit worth forming

Before writing a helper, spend sixty seconds asking whether the standard library already has it. It usually does, it is better tested than yours will be, and the next person reading your code already knows it.

5Mini-Project 2 — Contact Book

Your second graded deliverable · 100 points

Build a contact book that survives being closed. Everything in Weeks 1 and 2 comes together here: functions, lists, dictionaries, files and modules.

Required behaviour

  1. A menu loop: add, list, search, delete, quit.
  2. Each contact has a name, a phone number and an email.
  3. Contacts persist to contacts.csv and reload on the next run.
  4. Search matches part of a name, case-insensitively.
  5. Adding a duplicate name asks whether to update the existing entry.
  6. Bad input never crashes the program — including a missing or malformed CSV file.
  7. The code is split across at least two modules, with an importable one containing no I/O.
CriterionPoints
Menu loop and clean exit10
Add, list, search, delete all correct25
Persistence — saves and reloads correctly20
Handles missing / malformed / empty file15
Sensible module split; importable module does no I/O15
Naming, constants, docstrings, PEP 810
Comments where they earn their place5
The trap in this project

It is not the file handling. It is that you must decide what happens when the data on disk does not match what your code expects — a missing column, a blank line, a half-written file from a crash. Deciding that deliberately, and writing it down, is the assignment.

Suggested build order

  1. Hard-code three contacts in a list of dicts. Get list and search working with no files at all.
  2. Add the menu loop around it.
  3. Add saving. Check the CSV by opening it in a text editor.
  4. Add loading. Now quit and restart — this is the moment the project becomes real.
  5. Break it deliberately: delete the file, empty it, corrupt a row, add a stray blank line. Fix what breaks.
  6. Split into modules. Run black and ruff. Read it once as a stranger.
Step 1 is the one people skip

Get every operation right against a hard-coded list before a single file is opened. If something is wrong then, it is definitely the logic — there is nothing else in the program yet. It is the same discipline as Mini-Project 1, and it works for the same reason.

Keep this file too. On Day 12 you will turn each contact from a dictionary into an object, and on Day 15 you will write tests for it. Having the earlier version to compare against is, as ever, the lesson.

>_Python playground

A real Python interpreter running inside your browser. Nothing is installed, nothing is uploaded, nothing can break.

scratch.pypython not loaded
Values for input(), comma separated →
Output appears here. The first run takes a few seconds while Python loads.

Exercise set

Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.

All Warm-up Core Challenge Reset day

Day 11 — Algorithm Design & Analysis

Derive binary search, implement three sorts by hand, and read Big-O honestly.

Continue →