Learning Goals
3 min- Write a fresh CSV with
csv.writer— header + rows. - Write with
csv.DictWriter— supplyfieldnames, write dicts. - Tell the difference between
"w"(overwrite) and"a"(append). - Update existing rows with the read-modify-write pattern.
Warm-Up · Which Mode?
5 minLook at these three calls to open. What happens to an existing file in each case?
open("data.csv", "r") open("data.csv", "w") open("data.csv", "a")
Show the answer
"r"— read. File is opened for reading; if it doesn't exist, you getFileNotFoundError."w"— write. Truncates the file to empty the moment you open it. Be careful — there is no undo."a"— append. Existing rows are preserved; new writes go at the end.
To change existing rows in a CSV you don't edit in place. You read the whole file into memory, change the list, and write the whole file back out. That's the read-modify-write pattern, and it works for almost every file-update task.
New Concept · writer, DictWriter, append
14 mincsv.writer — write rows as lists
import csv rows = [ ["name", "age", "score"], # header ["Aisyah", 13, 88], ["Wei Jie", 14, 75], ["Suresh", 13, 92], ] with open("out.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerows(rows)
out.csv name,age,score Aisyah,13,88 Wei Jie,14,75 Suresh,13,92
writerows writes a list of rows at once. There's also writerow for a single row.
csv.DictWriter — write rows as dicts
import csv students = [ {"name": "Aisyah", "age": 13, "score": 88}, {"name": "Wei Jie", "age": 14, "score": 75}, {"name": "Suresh", "age": 13, "score": 92}, ] with open("out.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=["name", "age", "score"]) writer.writeheader() writer.writerows(students)
You always supply fieldnames — they become the header AND the column order. Anything not in fieldnames is dropped; anything missing is left blank.
Append mode — add to an existing file
# Don't write the header again — it's already on disk. with open("out.csv", "a", newline="") as f: writer = csv.DictWriter(f, fieldnames=["name", "age", "score"]) writer.writerow({"name": "Mei", "age": 14, "score": 80})
Append mode is perfect for log files: each run adds one row to the end.
Read-modify-write update
To raise Wei Jie's score by 5: read, change, rewrite.
import csv # 1. Read the whole file with open("out.csv", newline="") as f: rows = list(csv.DictReader(f)) # 2. Modify for r in rows: if r["name"] == "Wei Jie": r["score"] = str(int(r["score"]) + 5) # CSV stays strings # 3. Write everything back (overwrite) with open("out.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows)
This is the single most common file-editing pattern in software. Memorise it.
Worked Example · Attendance Logger
12 minAn attendance app records who showed up today. New runs append; we never lose history.
# attendance.py — append-only attendance log import csv from datetime import date from pathlib import Path FILE = Path("attendance.csv") FIELDS = ["date", "name", "status"] # Create with header if it doesn't exist yet if not FILE.exists(): with FILE.open("w", newline="") as f: csv.DictWriter(f, fieldnames=FIELDS).writeheader() # Today's roll-call today = str(date.today()) roll = ["Aisyah", "Wei Jie", "Suresh", "Mei"] with FILE.open("a", newline="") as f: writer = csv.DictWriter(f, fieldnames=FIELDS) for name in roll: status = input(f" {name}? (p/a): ").strip().lower() writer.writerow({ "date": today, "name": name, "status": "present" if status == "p" else "absent", }) print(f"\n📋 logged {len(roll)} entries to {FILE}")
Sample run
Aisyah? (p/a): p Wei Jie? (p/a): a Suresh? (p/a): p Mei? (p/a): p 📋 logged 4 entries to attendance.csv
attendance.csv after three days
date,name,status 2026-05-26,Aisyah,present 2026-05-26,Wei Jie,absent 2026-05-26,Suresh,present 2026-05-26,Mei,present 2026-05-27,Aisyah,present ...
Read the diff
Three smart choices: header-on-first-run, append mode for history, date.today() from the stdlib. Run the program any day and history grows. This is how production logs work — never overwrite, always append.
Try It Yourself
13 minMake a list of three favourite movies (title, year, rating). Save it as movies.csv with a header.
Hint
import csv movies = [ {"title": "Up", "year": 2009, "rating": 5}, {"title": "Inside Out","year": 2015, "rating": 5}, {"title": "Coco", "year": 2017, "rating": 4}, ] with open("movies.csv", "w", newline="") as f: w = csv.DictWriter(f, fieldnames=["title", "year", "rating"]) w.writeheader() w.writerows(movies)
Append two more movies to movies.csv WITHOUT writing the header again.
Hint
extra = [ {"title": "Toy Story 3", "year": 2010, "rating": 5}, {"title": "Brave", "year": 2012, "rating": 4}, ] with open("movies.csv", "a", newline="") as f: w = csv.DictWriter(f, fieldnames=["title", "year", "rating"]) w.writerows(extra) # NO writeheader()
Bump every 4-star movie up to 5 stars. Use read-modify-write.
Hint
with open("movies.csv", newline="") as f: rows = list(csv.DictReader(f)) for r in rows: if int(r["rating"]) == 4: r["rating"] = "5" with open("movies.csv", "w", newline="") as f: w = csv.DictWriter(f, fieldnames=rows[0].keys()) w.writeheader() w.writerows(rows)
Mini-Challenge · Daily Mood Tracker
8 minBuild mood.py. Each run:
- Asks "how are you today? (1-5)" and an optional one-line note.
- Appends a row to
mood.csvwith today's date, the rating and the note. - Reads the whole file and reports: total entries, average rating, the day with the highest rating so far.
Show one possible solution
# mood.py — append + summarise import csv from datetime import date from pathlib import Path FILE = Path("mood.csv") FIELDS = ["date", "rating", "note"] if not FILE.exists(): with FILE.open("w", newline="") as f: csv.DictWriter(f, fieldnames=FIELDS).writeheader() rating = int(input("Mood today (1-5): ")) note = input("One line about it: ").strip() with FILE.open("a", newline="") as f: csv.DictWriter(f, fieldnames=FIELDS).writerow({ "date": str(date.today()), "rating": rating, "note": note, }) # Summarise with FILE.open(newline="") as f: rows = list(csv.DictReader(f)) ratings = [int(r["rating"]) for r in rows] best = max(rows, key=lambda r: int(r["rating"])) print(f"\nEntries: {len(rows)}") print(f"Average: {sum(ratings)/len(ratings):.2f}") print(f"Best day so far: {best['date']} (rating {best['rating']})")
Non-negotiables: file header on first run, append for new entries, read-back for the summary stats.
Recap
3 mincsv.writer takes lists; csv.DictWriter takes dicts (cleaner, name-based). Mode "w" truncates and rewrites; "a" appends. To change existing rows, read the whole file, modify the in-memory list, write everything back. Tomorrow we use these skills to build a CSV ↔ JSON converter.
Vocabulary Card
- writeheader / writerow / writerows
- DictWriter methods — write the header line, one row, many rows.
- append mode
open(path, "a"). Existing content kept; new writes go to the end.- read-modify-write
- Standard pattern for editing a file: load everything, change in memory, save everything back.
Homework
4 minBuild todo.py:
- Stores tasks in
todo.csvwith columnsid, task, done. - The program offers a menu: 1) list tasks, 2) add task, 3) mark task done, 4) quit.
- "done" is stored as the string
"yes"or"no". - Use append for adds; use read-modify-write for "mark done".
Sample · todo.py
# todo.py — CSV-backed to-do list import csv from pathlib import Path FILE = Path("todo.csv") FIELDS = ["id", "task", "done"] def load(): if not FILE.exists(): return [] with FILE.open(newline="") as f: return list(csv.DictReader(f)) def save(rows): with FILE.open("w", newline="") as f: w = csv.DictWriter(f, fieldnames=FIELDS) w.writeheader() w.writerows(rows) def list_tasks(rows): if not rows: print(" (no tasks yet)") return for r in rows: tick = "✅" if r["done"] == "yes" else "⬜" print(f" {tick} #{r['id']} {r['task']}") def add_task(rows): task = input(" task: ").strip() new_id = str(max([int(r["id"]) for r in rows], default=0) + 1) rows.append({"id": new_id, "task": task, "done": "no"}) save(rows) print(f" added #{new_id}") def mark_done(rows): tid = input(" id to mark done: ").strip() for r in rows: if r["id"] == tid: r["done"] = "yes" save(rows) while True: rows = load() print("\n1) list 2) add 3) done 4) quit") choice = input("> ").strip() if choice == "1": list_tasks(rows) elif choice == "2": add_task(rows) elif choice == "3": mark_done(rows) elif choice == "4": break
Non-negotiables: persistent storage, growing ids, mark-done updates the file, file is created if missing.