Learning Goals
3 minBy the end of this lesson you can:
- Write a single line with
f.write("text\\n")— and remember the explicit\\n. - Choose between mode
"w"(overwrite) and mode"a"(append) — and predict which one will keep your old data. - Save many lines at once with
f.writelines(lst). - Round-trip a list to disk and back: write it out, read it in, compare.
Warm-Up · The Two Modes
5 minMake a fresh folder for this lesson. Don't create the file by hand — Python is about to.
Predict what's in diary.txt after running this:
# Run #1 — start fresh, write three lines with open("diary.txt", "w") as f: f.write("Day 1: started Python.\n") f.write("Day 2: built Mad Libs.\n") f.write("Day 3: learned about dicts.\n") # Run #2 — open in "a" mode and add one more with open("diary.txt", "a") as f: f.write("Day 4: saved my first file!\n")
Show the file contents
Day 1: started Python. Day 2: built Mad Libs. Day 3: learned about dicts. Day 4: saved my first file!
Run #1 created the file. Run #2 added a line. If we'd used "w" in run #2 instead of "a", the first three lines would be gone. "w" means "start a brand-new file".
Two letters change the world. "w" = write from scratch (file wiped). "a" = append to the end (file kept). Pick the wrong one and you delete your high scores.
New Concept · Three Write Patterns
14 minThe mode argument
open(name) same as open(name, "r") → read text open(name, "w") write text — file wiped clean open(name, "a") append text — original kept, new lines added at end open(name, "x") create-new only — crashes if file exists open(name, "r+") read and write (rarely needed)
For now you only need "r", "w" and "a". "x" is occasionally handy when you want to be sure you're not overwriting.
1 · f.write(text)
The basic call. Pass a string. Python writes it as-is — no newline added. If you want lines, put \\n at the end yourself.
with open("greetings.txt", "w") as f: f.write("Hello\n") f.write("Hello again\n") f.write("Last line\n")
Output file:
Hello Hello again Last line
Forget the \\n and all three lines collide on one row — HelloHello againLast line. The newline is on you.
2 · f.writelines(list_of_lines)
Pass a list of strings — they all get written one after another. Same rule: no newlines are added for you.
students = ["Aisyah\n", "Wei Jie\n", "Priya\n", "Iman\n"] with open("class.txt", "w") as f: f.writelines(students)
If your list doesn't already have \\n on each item, glue them on in one line with a list comprehension:
students = ["Aisyah", "Wei Jie", "Priya", "Iman"] with open("class.txt", "w") as f: f.writelines([name + "\n" for name in students])
Or use the .join trick from PY-L2-14 with a single .write:
with open("class.txt", "w") as f: f.write("\n".join(students) + "\n")
3 · print(..., file=f)
Surprise: print can write to a file. Pass file= and it writes there instead of the screen — and adds the newline for you.
with open("log.txt", "w") as f: print("Booting up...", file=f) print("Loaded 42 items", file=f) print("Done.", file=f)
Same output as three f.write("...\\n") calls, but no manual newlines. A favourite trick for quick logs.
The append pattern · saving high scores
The "a" mode is the "diary" pattern. Run after run, the file grows.
from datetime import datetime score = 95 when = datetime.now().isoformat(timespec="seconds") with open("scores.txt", "a") as f: f.write(f"{when} | score {score}\n")
Run it three times — three lines appear. The same code never wipes earlier runs.
Round-trip — write then read
Sanity-check what you wrote by reading it back:
with open("class.txt", "w") as f: f.writelines([n + "\n" for n in ["Aisyah", "Wei Jie"]]) with open("class.txt") as f: print(f.read()) # → Aisyah # Wei Jie
"w" wipes the file the moment open returns — even before you write the first byte. If your program crashes after opening but before writing, the file is now blank. For really important data, write to a temp file first and rename it at the end. We'll do that properly in Level 4; for now, be careful with "w".
Worked Example · The Mini Diary App
12 minThe story
Build a tiny diary that the user can use any number of times. Each entry adds to diary.txt. A second mode lets them read everything they've written so far.
Save as diary.py:
Code
# diary.py — append-only diary app from datetime import datetime def add_entry(): print() print("Type your entry. Press Enter on an empty line to save.") lines = [] while True: line = input() if line == "": break lines.append(line) if len(lines) == 0: print("(nothing to save)") return now = datetime.now().isoformat(timespec="minutes") with open("diary.txt", "a", encoding="utf-8") as f: f.write(f"--- {now} ---\n") for line in lines: f.write(line + "\n") f.write("\n") print("Saved.") def show_diary(): print() try: with open("diary.txt", encoding="utf-8") as f: print(f.read()) except FileNotFoundError: print("(no entries yet)") while True: pick = input("1 add 2 read q quit: ").lower() if pick == "1": add_entry() elif pick == "2": show_diary() elif pick == "q": print("Bye!") break else: print("Pick 1, 2 or q.")
Sample session
1 add 2 read q quit: 1 Type your entry. Press Enter on an empty line to save. Built my first diary app. It saves between runs! Saved. 1 add 2 read q quit: 2 --- 2026-05-27T18:42 --- Built my first diary app. It saves between runs! 1 add 2 read q quit: q Bye!
Read the diff
One file. Two modes used. add_entry uses "a" so old entries are kept. show_diary uses default read mode. The try/except FileNotFoundError handles the first-ever run, when the file doesn't exist yet. The datetime timestamp comes from a stdlib module we'll dive into in PY-L2-32.
\\n?The diary writes a header line, several content lines, and a blank line between entries. Every one of those needs its own explicit \\n. Forget any of them and entries glue together. Always add the newline yourself — f.write() never does it for you.
Try It Yourself
13 minWrite a list of three favourite foods to foods.txt. Try it once with three f.write calls, then again with one f.writelines. Confirm the file looks the same.
Hint
foods = ["nasi lemak", "satay", "cendol"] with open("foods.txt", "w") as f: for food in foods: f.write(food + "\n") # Same file, written a second way with open("foods.txt", "w") as f: f.writelines([food + "\n" for food in foods])
Both produce the same file. Use whichever feels clearer for the size of the list — writelines wins when the list is already in hand.
Save the list ["Aisyah", "Wei Jie", "Priya", "Iman"] to names.txt, then read it back into a fresh variable and confirm the two lists are equal.
Hint
names = ["Aisyah", "Wei Jie", "Priya", "Iman"] with open("names.txt", "w") as f: for n in names: f.write(n + "\n") with open("names.txt") as f: loaded = [line.rstrip("\n") for line in f] print("Original:", names) print("Loaded :", loaded) print("Match? :", names == loaded)
The round-trip test is your safety net: if it's not True, something's off — usually a forgotten \\n or a missing strip.
Build log_session.py. Every time you run it, it asks the user for one sentence and appends it to session_log.txt with a timestamp. Run the file three times; check that all three lines are kept.
Hint
from datetime import datetime text = input("Today's note: ") stamp = datetime.now().isoformat(timespec="seconds") with open("session_log.txt", "a", encoding="utf-8") as f: f.write(f"[{stamp}] {text}\n") print("Saved.")
This is the smallest useful diary app you can write. Six lines of code, persistent storage between runs.
Mini-Challenge · The Sticky-Note CLI
8 minBuild notes.py. A four-option menu app for a one-line sticky note list, all backed by notes.txt.
Your file must:
1Add — ask for a note, append it to the file with a number prefix and timestamp.2List — print every note on its own line. Show(no notes yet)if the file is empty or missing.3Clear — wipe the file with mode"w"and an empty write. Confirm with the user first!4Quit.
Stretch goal. Track the note number in the file. The first note is #1, the second is #2 etc. — load the file at start to find the next number.
Show one possible solution
# notes.py — sticky-note CLI from datetime import datetime FILE = "notes.txt" def next_number(): try: with open(FILE, encoding="utf-8") as f: return sum(1 for _ in f) + 1 except FileNotFoundError: return 1 def add(): n = next_number() text = input("Note: ") when = datetime.now().isoformat(timespec="minutes") with open(FILE, "a", encoding="utf-8") as f: f.write(f"#{n:03} [{when}] {text}\n") print(f"Saved #{n:03}.") def show(): try: with open(FILE, encoding="utf-8") as f: content = f.read() if content == "": print("(no notes yet)") else: print(content, end="") except FileNotFoundError: print("(no notes yet)") def clear(): sure = input("Wipe all notes? (yes/no): ") if sure == "yes": with open(FILE, "w", encoding="utf-8") as f: f.write("") print("Cleared.") else: print("Not cleared.") while True: pick = input("1 add 2 list 3 clear 4 quit: ") if pick == "1": add() elif pick == "2": show() elif pick == "3": clear() elif pick == "4": print("Bye!") break else: print("Pick 1-4.")
Non-negotiables: append mode for adding, default read for listing, write mode for clearing (with confirmation), and a numbered-note format. The next_number function reads the existing file to count entries — that's the "state-aware" bit that makes notes feel like a real app.
Recap
3 minWriting files is one letter different from reading. open(name, "w") wipes the file and writes from scratch. open(name, "a") appends to the end and keeps everything that was there. f.write(text) writes one string — without a newline, so add \\n yourself. f.writelines(list) writes many at once. print(..., file=f) writes through print's nicer interface (and adds the newline for free). Round-trip your output to be sure it loads back the way you saved it.
Vocabulary Card
- "w" mode
- Write text. Wipes the file the moment you open it.
- "a" mode
- Append text. Keeps existing content; new writes go at the end.
- f.write(text)
- Write one string. No newline added — add
\\nyourself. - f.writelines(list)
- Write a list of strings. No newlines added between them.
- print(..., file=f)
- Send
print's output to a file instead of the screen. Adds\\nfor you. - round-trip
- Write, then read back. The two should match — your safety check.
Homework
4 minBuild todo_persist.py — your todo.py from PY-L2-01 with a saved list. Keep the task list in tasks.txt; load it at start, write it back after every change.
Your file must:
- At the start:
tryto readtasks.txt; if it doesn't exist, start with an empty list. - Inside the menu loop: add / done / sort / quit. After every change that affects the list (add, done, sort), save the whole list to disk using mode
"w". - Make sure that exiting and re-running the program shows the same tasks.
Stretch. Add a third file completed.txt — every time a user marks a task done, append the task with a timestamp to that file.
Sample · todo_persist.py
# todo_persist.py — to-do list that survives between runs from datetime import datetime FILE = "tasks.txt" DONE = "completed.txt" def load_tasks(): try: with open(FILE, encoding="utf-8") as f: return [line.rstrip("\n") for line in f if line.strip()] except FileNotFoundError: return [] def save_tasks(tasks): with open(FILE, "w", encoding="utf-8") as f: for t in tasks: f.write(t + "\n") def archive_done(task): stamp = datetime.now().isoformat(timespec="minutes") with open(DONE, "a", encoding="utf-8") as f: f.write(f"[{stamp}] {task}\n") tasks = load_tasks() print(f"Loaded {len(tasks)} task(s).") while True: print() print("Tasks:", tasks) pick = input("1 add 2 done 3 sort 4 quit: ") if pick == "1": t = input("New task: ") tasks.append(t) save_tasks(tasks) elif pick == "2": t = input("Done with which? ") if t in tasks: tasks.remove(t) archive_done(t) save_tasks(tasks) else: print("Not on the list.") elif pick == "3": tasks.sort() save_tasks(tasks) elif pick == "4": print("Bye!") break else: print("Pick 1-4.")
Non-negotiables: load at start (with try/except), save after every change, and append the completed task to a separate log. This is the bedrock pattern for every "data persists between runs" app you'll ever write — until JSON makes it even cleaner in PY-L2-45.