Python module 2: loops, lists, dictionaries, files
Fourteen sessions to go from toy program to useful program: make the machine repeat (loops), store collections of data (lists, dictionaries), read and write real files, and survive errors. By the end of the module you will read a CSV file line by line and pull totals out of it.
Day 15 — The while loop: repeat as long as
Goal: repeat a block as long as a condition is true, without ever writing an infinite loop by accident.
Theory (20 min)
A computer never gets bored: that is its whole value. The while loop repeats an indented block as long as its condition is true, exactly like an if… except that at the end of the block, we go back up and test the condition again:
counter = 1
while counter <= 3:
print(f"Round {counter}")
counter = counter + 1
print("Done.")
Prints Round 1, Round 2, Round 3, then Done. The variable counter is initialised before the loop, tested in the condition, and modified at the end of the block. If you forget the modification, the condition stays true forever: that is the infinite loop, the program spins without stopping — interrupt it with Ctrl+C in the terminal.
Two control commands inside a loop: break leaves the loop immediately, continue jumps straight to the next round. Today's classic pattern:
while True:
answer = input("Again? (yes/no) ")
if answer == "no":
break
print("Goodbye.")
An always-true condition, and an explicit way out through break: this is the standard shape of a program that talks until you tell it to stop.
What happens if you forget counter = counter + 1 in the first example?
- Infinite loop: "Round 1" prints forever
- An error
- The loop never starts
Answer
The condition stays true forever: the loop never ends. Ctrl+C to interrupt it.
What does break do?
- It jumps to the next round
- It leaves the loop immediately
- It stops the whole program
Answer
break leaves the loop; execution continues after it. (continue, on the other hand, jumps to the next round.)
Practice (30 min)
- Retype the first example into
count.py, run it. Then delete the increment line, rerun, let "Round 1" scroll for three seconds, interrupt with Ctrl+C: you can now recognise and stop an infinite loop. Put the line back. - Retype the second example into
dialogue.py, run it, answer "yes" twice then "no". - The higher-or-lower game, in
mystery.py:
secret = 7
tries = 0
while True:
guess = int(input("Guess the number (1-10): "))
tries = tries + 1
if guess < secret:
print("Higher!")
elif guess > secret:
print("Lower!")
else:
print(f"Won in {tries} tries!")
break
- Success criterion: the game guides you ("higher" / "lower") until you win, and announces the number of tries. Play two rounds.
Agent tips: "write me a higher-or-lower game" is tempting; doing it yourself takes ten minutes and installs the while True + break structure for good. If you delegate anyway: classic trap, the agent forgets int() around input. Proof: play a full game, not just a launch.
Recap (10 min)
Three lines: the three roles of the loop variable (init, test, modify), break vs continue, Ctrl+C. Without notes: a loop that prints the squares of 1 to 5 (1, 4, 9, 16, 25).
Day 16 — The for loop and range(): repeat a known number of times
Goal: choose between for and while, and master range().
Theory (20 min)
When you know how many times you want to repeat — or what you want to repeat over — the for loop is simpler than while: no counter to manage, no risk of an infinite loop. range(n) produces the numbers from 0 to n-1:
for i in range(5):
print(i)
Prints 0, 1, 2, 3, 4. It starts at 0 and stops before the end: range(5) does not contain 5. That is Python's convention everywhere, and the source of the most common "off-by-one" bug in programming. Variants: range(1, 6) gives 1 to 5; range(0, 10, 2) gives 0, 2, 4, 6, 8 (the third number is the step).
Read it as "for each": for i in range(5) reads "for each i in 0, 1, 2, 3, 4". The variable (here i) takes a new value each round, automatically.
for or while? Simple rule: known number of rounds → for; repeat until something happens → while. Counting from 1 to 10: for. Asking for a password until it is right: while.
How many numbers does range(4) produce?
- 3
- 4
- 5
Answer
4: 0, 1, 2, 3. It starts at 0 and the upper bound is excluded.
To print the multiples of 3 up to 30, the best loop is…
- while, because we do not know the end
- while True with break
- for with range(3, 31, 3)
Answer
We know the exact values: for, with its step of 3.
Practice (30 min)
- In the REPL:
list(range(5)),list(range(1, 6)),list(range(10, 0, -1))—list()makes visible what range produces. Note the negative step of the third. - Create
tables.py:
table = int(input("Times table for? "))
for i in range(1, 11):
print(f"{table} x {i} = {table * i}")
- Success criterion: with 7, ten lines print from "7 x 1 = 7" to "7 x 10 = 70".
- Add
total = 0before the loop andtotal = total + iinside it, then a final print: you have just written an accumulator, the pattern used to total anything (it returns on days 27 and 28). - Rewrite yesterday's higher-or-lower game with
for attempt in range(5):— five tries at most — and a "You lost!" message if the loop ends without a break.
Agent tips: "convert this while into a for" is a good refactor to delegate. Trap: range(1, n) forgets the last one — the off-by-one is the bug an agent misses most often. Proof: count the printed lines; there must be exactly the expected number.
Recap (10 min)
Three lines: why range excludes the end should no longer surprise you, for vs while, what an accumulator is. Without notes: a for loop that prints the sum of the numbers 1 to 100 (check: 5050).
Day 17 — Lists: storing several values
Goal: create a list, reach its elements by position, extract slices.
Theory (20 min)
A list stores several values in a single variable, between square brackets, separated by commas:
shopping = ["bread", "milk", "eggs", "coffee"]
print(shopping)
Each element has a position, called an index, and — same convention as range — the first element is at index 0. shopping[0] is "bread", shopping[1] is "milk". len(shopping) gives the number of elements (4), so the last valid index is len - 1 (3). Asking for shopping[4] raises an IndexError: list index out of range — remember that message, you will meet it.
Special index: shopping[-1] gives the last element, shopping[-2] the one before. Handy when you do not know the length.
Slices extract a piece: shopping[1:3] gives ["milk", "eggs"] — start included, end excluded, like range. shopping[:2] the first two, shopping[2:] from the third on.
And the for loop finds its full meaning: it walks a list directly, without an index:
for item in shopping:
print(f"- {item}")
With days = ["mon", "tue", "wed"], what is days[1]?
- "mon"
- "tue"
- An IndexError
Answer
Indexes start at 0: days[0] is "mon", days[1] is "tue".
How do you get the last element of a list without knowing its length?
- mylist[-1]
- mylist[len]
- mylist[0]
Answer
Index -1 always points to the last element.
Practice (30 min)
- In the REPL, create
days = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]and test:days[0],days[-1],days[5:](the weekend!),days[:5],len(days). Thendays[7]: read the IndexError. - Create
playlist.py:
tracks = ["Song to Song", "Midnight Sun", "River", "Lullaby"]
print(f"{len(tracks)} tracks:")
for t in tracks:
print(f"♪ {t}")
print(f"Next listen: {tracks[0]}")
- Success criterion: the program prints the number of tracks, the bulleted list, and the first track — change the list, everything follows.
- Add
print(tracks[1:3])and check that the end is indeed excluded.
Agent tips: "why does mylist[3] crash when my list has 3 elements?": the agent answers "indexes start at 0" in one line. Trap: it may suggest mylist[len(mylist)] which also crashes. Proof: the last printed value is indeed the last element of your list.
Recap (10 min)
Three lines: index 0, index -1, slice start included / end excluded. Without notes: a list of three films, print the second one and the last two.
Day 18 — Lists: modify, sort, iterate
Goal: add, remove, sort elements, and build a list inside a loop.
Theory (20 min)
Lists can be modified after creation — that is what sets them apart from strings. Today's methods:
mylist.append(x)adds x at the end;mylist.remove(x)removes the first occurrence of x (error if absent);mylist.pop()removes and returns the last element;mylist.sort()sorts in place (modifies the list);sorted(mylist)returns a new sorted list without touching the original;x in mylisttests membership (like for strings on day 8).
The most useful pattern of everyday programming: building a list inside a loop. Start from an empty list, fill it:
doubles = []
for n in range(1, 6):
doubles.append(n * 2)
print(doubles)
Prints [2, 4, 6, 8, 10]. Empty list + loop + append: this skeleton filters, transforms, collects — you will reuse it in the CSV parser of day 27.
Beware a famous trap: b = a does not copy a list, it sticks a second label on the same box. Modifying b modifies a. To really copy: b = a.copy() or b = a[:].
After a = [1, 2], b = a, b.append(3), what is a?
- [1, 2]
- An error
- [1, 2, 3]
Answer
b = a names the same list twice: appending to b is visible from a. To copy: a.copy().
Difference between mylist.sort() and sorted(mylist)?
- sort() modifies the list, sorted() returns a new one
- None, they are two names for the same thing
- sorted() sorts in reverse
Answer
sort() works in place and returns None; sorted() leaves the original untouched.
Practice (30 min)
- Create
scores.py:
scores = []
while True:
entry = input("A score (or 'done')? ")
if entry == "done":
break
scores.append(int(entry))
scores.sort()
print(f"Scores: {scores}")
print(f"Best: {scores[-1]}")
- Success criterion: enter 12, 4, 27, 8 then "done": the list prints sorted and the best score is 27 — collect, sort and read the last element in one program.
- Reproduce the alias trap:
a = [1, 2],b = a,b.append(3), printa. Then redo withb = a.copy()and see the difference. - Add to the program: after the sort,
scores.remove(min(scores))then print again —min()andmax()give the smallest / largest element.
Agent tips: "my append on b also changes a, is Python broken?": the agent explains aliasing. Trap: it sometimes suggests copy.deepcopy, overkill for a simple list. Proof: print the original after modifying the copy.
Recap (10 min)
Three lines: append vs remove vs pop, sort vs sorted, why b = a does not copy. Without notes: a loop that builds the list of squares from 1 to 10, then prints its last element.
Day 19 — String methods: split, clean, rejoin
Goal: clean and split real-world text: spaces, case, separators.
Theory (20 min)
Real files are dirty: extra spaces, random capitals, fields glued with commas. Four string methods do 90% of the cleaning:
.strip()removes spaces and line breaks at both ends:" bread \n".strip()→"bread";.lower()/.upper()normalise case before comparing;.split(",")cuts into a list at each separator:"a,b,c".split(",")→["a", "b", "c"]. With no argument,.split()cuts on whitespace;", ".join(mylist)does the reverse: glues a list back into one string:", ".join(["a", "b"])→"a, b".
Two properties to know: a string cannot be modified (it is immutable) — word.upper() does not change word, it returns a new string you must store: word = word.upper(). And a string iterates like a list: for letter in "abc" runs three times; "abc"[0] is "a".
Also useful: .startswith("http") and .endswith(".pdf") test the start and the end — they will serve the file sorter of module 3. And .replace("a", "b") replaces every occurrence.
"Jo,25,Lyon".split(",") gives…
- The string "Jo 25 Lyon"
- The list ["Jo", "25", "Lyon"]
- The list ["Jo,25,Lyon"]
Answer
split cuts at each comma and returns a list of three strings — the "25" stays text, convert with int() if needed.
After word = "abc" then word.upper(), what is word?
- "abc" — upper() returns a new string without modifying word
- "ABC"
- An error
Answer
Strings are immutable: store the result, word = word.upper().
Practice (30 min)
- In the REPL:
" Alice ".strip(),"a b c".split(),"-".join(["2026", "11", "03"]),"photo.pdf".endswith(".pdf"). - Create
cleanup.py:
dirty_line = " SMITH, alice , LYON "
pieces = dirty_line.split(",")
clean = []
for p in pieces:
clean.append(p.strip().title())
print(clean)
print(" | ".join(clean))
- Success criterion: the program prints
['Smith', 'Alice', 'Lyon']thenSmith | Alice | Lyon— the dirty string became three clean fields (.title()capitalises the first letter). - Change the dirty line (other spaces, other case): the result stays clean. That is what a robust program is.
- Ask the user for the line with input() instead of hardcoding it.
Agent tips: "which str methods clean up X": the agent lists them well. Trap: it invents methods that do not exist (.trim() does not exist in Python — that is JavaScript). Proof: run each proposed method in the REPL before adopting it.
Recap (10 min)
Three lines: strip/split/join, the immutability of strings, startswith/endswith. Without notes: turn " smith ; alice " into "Smith / Alice".
Day 20 — Tuples and unpacking
Goal: meet the list's immutable cousin, and unpack several values at once.
Theory (20 min)
A tuple is like a list, but frozen: written between parentheses, and once created you can neither add, nor remove, nor change an element:
point = (3, 4)
print(point[0])
What is a list you cannot modify good for? For saying "these values belong together and will not change": coordinates, a date, an (red, green, blue) colour. Python uses them everywhere internally, and you will meet them in libraries.
The feature that makes tuples addictive is unpacking: distributing the elements into several variables in one go:
width, height = (1920, 1080)
print(width)
It also works without parentheses — that is even the usual style: x, y = 3, 4. And it enables the one-line swap, impossible in most languages: a, b = b, a.
Unpacking also applies to lists and to split's return value: name, age = "Alice,30".split(",") stores "Alice" in name and "30" in age — careful, the element count must match exactly, otherwise ValueError. This shortcut will serve on day 27 for the CSV.
Essential difference between tuple and list?
- The tuple cannot be modified after creation
- The tuple is faster to type
- The list can only hold numbers
Answer
Immutability: no append, no point[0] = 5 on a tuple.
After a, b = 1, 2 then a, b = b, a, what is a?
- 1
- 2
- An error
Answer
Unpacking evaluates the whole right side first: the swap works in one line.
Practice (30 min)
- In the REPL:
colour = (255, 128, 0),colour[1], thencolour[0] = 100: read the TypeError confirming immutability. Thenr, g, b = colourand print g. - Write the swap
a, b = b, ain the REPL and check it. - Create
directory.py:
contacts = [("Alice", "0601"), ("Bob", "0602"), ("Cloe", "0603")]
for name, phone in contacts:
print(f"{name}: {phone}")
- Success criterion: three "name: number" lines print — the loop unpacks each tuple straight into
nameandphone, no index. - Add a fourth contact and rerun.
- Try
name, phone, extra = contacts[0]: read the ValueError "not enough values to unpack", write it in the notebook.
Agent tips: "turn these index-based loops into unpacking" — an agent modernises that very well. Trap: it can break the arity (unpacking 3 values from a 2-tuple). Proof: rerun on your data; any mismatch is a visible ValueError.
Recap (10 min)
Three lines: tuple vs list, unpacking, the one-line swap. Without notes: a list of three (city, population) tuples, a loop that prints "city — population".
Day 21 — Dictionaries: key-value pairs
Goal: store named data and reach it by name.
Theory (20 min)
A list finds its elements by position. A dictionary finds them by a name you choose — the key. It is written between braces, with key: value pairs:
alice = {"age": 30, "city": "Lyon", "job": "nurse"}
print(alice["city"])
alice["city"] reads "in alice, the value stored under the key "city"". Like a real dictionary: you look up a word (the key), you find its definition (the value). Keys are strings most of the time; values can be anything — numbers, strings, lists, other dictionaries.
Create, read, modify, add, delete:
- add or modify:
alice["age"] = 31(if the key exists, replace; otherwise create); - delete:
del alice["job"]; - test:
"city" in alice→ True; - reading an absent key,
alice["nickname"]→ KeyError, the sixth error in your notebook.
When to pick a dictionary over a list? List: a collection of same-kind elements whose order matters (scores, files). Dictionary: a record with named fields (a person, a product, a configuration). Most real-world data is lists of dictionaries: a table where each row is a record.
{"a": 1}["b"] raises…
- IndexError
- TypeError
- KeyError
Answer
The key "b" does not exist: KeyError. (Tomorrow's .get() avoids it.)
How do you add the key "email" to a dictionary d?
- d.append("email", "...")
- d["email"] = "..."
- d.add("email", "...")
Answer
Assign to the key: d["email"] = value creates or replaces.
Practice (30 min)
- In the REPL, create the alice dictionary from the lesson, read
alice["age"], modify it, setalice["city"] = "Paris"then add aalice["nickname"]key with a value of your choice. Test"city" in alice, then read an absent key to see the KeyError. - Create
inventory.py:
stock = {"apples": 12, "bananas": 5, "oranges": 0}
fruit = input("Which fruit? ").lower()
if fruit in stock:
print(f"{stock[fruit]} {fruit} left.")
else:
print("Not in stock.")
- Success criterion: "apples" prints 12, "kiwis" prints "Not in stock." — no KeyError, thanks to the
intest. - After the read, add:
stock[fruit] = stock[fruit] - 1in the "in stock" branch, and print the whole new stock.
Agent tips: "here is my data structure, list or dict?": the agent decides well given a concrete example. Trap: it answers "it depends" if you do not give your real data — paste three rows of it. Proof: reading your data must fit in one readable line.
Recap (10 min)
Three lines: key vs index, create/modify/delete, when to prefer a dict. Without notes: a product → price dict, a program that asks for a product and prints its price or "unknown".
Day 22 — Dictionaries: iterate, get, count
Goal: iterate a dictionary cleanly and count occurrences, the most useful pattern of the path.
Theory (20 min)
Iterating a dictionary: three methods. d.keys() the keys, d.values() the values, d.items() both as tuples — and that is the one used all the time, with day 20's unpacking:
stock = {"apples": 12, "bananas": 5}
for fruit, quantity in stock.items():
print(f"{fruit}: {quantity}")
Second tool: .get(key, default) reads a key safely — if it is absent, it returns the default instead of raising a KeyError:
print(stock.get("kiwis", 0))
Prints 0 without crashing. With these two tools, here is the counting pattern, probably the most useful of the whole path — counting occurrences of anything:
words = ["cat", "dog", "cat", "bird", "cat"]
count = {}
for word in words:
count[word] = count.get(word, 0) + 1
print(count)
Prints {'cat': 3, 'dog': 1, 'bird': 1}. For each word: "take the current count (0 if never seen), add 1, store". Remember this three-line skeleton: it will serve for day 27's expenses, module 3's file extensions, and half of all data processing that exists.
What does {"a": 1}.get("b", 0) return?
- 0
- A KeyError
- None
Answer
get with a default never crashes: it returns the default when the key is missing.
To get key AND value in a loop, write…
- for k, v in d:
- for k, v in d.items():
- for k, v in d.keys():
Answer
items() yields the pairs, unpacked into two variables.
Practice (30 min)
- Retype the lesson's counter into
counter.py, run it, check the result. - Success criterion: the printed dictionary gives the right frequencies; add a word to the list, rerun, the count follows.
- Variant: count the letters of a sentence typed by the user (
for letter in sentence.lower():, skip spaces with an if). - Create
bill.py: a dictprices = {"bread": 1.2, "milk": 1.5, "coffee": 4.9}, a listbasket = ["bread", "bread", "coffee"], and an accumulator that computes the total by consulting the dict. PrintTotal: 7.30 €with a:.2ff-string.
Agent tips: "count the occurrences of X in this list": the agent may pull out collections.Counter — it exists and it is great, but writing the counter by hand first is the exercise. Trap: accepting Counter without understanding the get() pattern. Proof: rewrite the manual version and compare both outputs.
Recap (10 min)
Three lines: items() + unpacking, get vs KeyError, the counting pattern in three lines. Without notes: count the vowels of "programming".
Day 23 — Sets: uniqueness for free
Goal: remove duplicates and run set operations (union, intersection).
Theory (20 min)
A set is a collection with no duplicates and no order: each value exists in it only once. Create one with braces (like a dict, but without the colons) or from a list:
guests = ["Alice", "Bob", "Alice", "Cloe", "Bob"]
unique = set(guests)
print(unique)
print(len(unique))
Prints a set of three names — the duplicates vanished effortlessly. That is use number one of a set: deduplicating. Careful: a set does not guarantee order; if order matters, go back through a list.
Set-theory operations, as learned at school:
- union (
|): everything in either one; - intersection (
&): what is in both; - difference (
-): what is in the first but not the second.
a = {"Alice", "Bob", "Cloe"}
b = {"Bob", "David"}
print(a & b)
print(a - b)
Prints {'Bob'} then {'Alice', 'Cloe'}. And the membership test x in myset is very fast, much faster than on a list — useful when checking thousands of values (day 41).
What is len(set([1, 2, 2, 3, 3, 3]))?
- 6
- An error
- 3
Answer
The set only keeps distinct values: 1, 2, 3.
What does a set NOT guarantee?
- The order of elements
- The absence of duplicates
- The membership test with in
Answer
A set has no order: do not rely on the display order.
Practice (30 min)
- In the REPL:
set("mississippi")— the distinct letters in one go. Thenlen(set("mississippi")). - Create
parties.py:
friday = {"Alice", "Bob", "Cloe", "David"}
saturday = {"Bob", "David", "Emma"}
print(f"Both nights: {friday & saturday}")
print(f"Friday only: {friday - saturday}")
print(f"At least one night: {friday | saturday}")
print(f"Total faces: {len(friday | saturday)}")
- Success criterion: the three printed sets match your hand computation (check each on paper), and the total is 5.
- Realistic exercise: a hardcoded list of 10 words with duplicates; print "10 words typed, 7 distinct" (adjust to the real count) with len() and len(set()).
Agent tips: "my list has duplicates" → the agent answers set() in one line. Trap: it forgets to warn that order is lost. Proof: print before/after and check the distinct element count.
Recap (10 min)
Three lines: what a set is for, the three operations, the order trap. Without notes: the intersection of two weekly shopping lists.
Day 24 — Reading a text file
Goal: open a file, read its content line by line, and always close it properly.
Theory (20 min)
Until now, your data died when the program ended. Files make it survive: they are the boxes where the computer keeps data between two runs. Reading a text file in Python fits in three lines, with the with keyword:
with open("notes.txt", encoding="utf-8") as f:
for line in f:
print(line.strip())
Let us dissect. open() opens the file and returns a file object, stored here in f. encoding="utf-8" says how characters are coded into bytes — utf-8 is the universal standard; without it, your accents may come out as hieroglyphs on another machine. with ... as ... guarantees the file will be closed at the end of the indented block, even if an error occurs: always open with with. Finally, a file iterates line by line like a list — and each line keeps its trailing line break (\n), hence day 19's .strip().
If the file does not exist: FileNotFoundError. Two reflexes: check the terminal is in the right folder (cd, day 2), and check the exact spelling of the name.
Useful variants: f.read() reads the whole file at once into a single string; f.readlines() returns the list of lines. For big files, stick to the line-by-line loop: it never loads everything into memory.
Why open a file with with?
- It is faster
- The file is closed automatically, even if an error occurs
- It skips the encoding
Answer
with guarantees closing whatever happens inside the block.
Why does each read line print an extra blank line with print?
- Each line carries its trailing \n, and print adds one — hence .strip()
- Python reads lines twice
- The file is badly encoded
Answer
The line's \n plus print's own makes a blank line: strip() cleans it.
Practice (30 min)
- Create by hand, in your editor, a file
expenses.txt:
bread 1.20
cinema 9.50
book 14.00
coffee 2.10
- Create
read.pyin the same folder:
total = 0.0
with open("expenses.txt", encoding="utf-8") as f:
for line in f:
name, amount = line.strip().split()
total = total + float(amount)
print(f"{name}: {amount} €")
print(f"Total: {total:.2f} €")
- Success criterion: four lines print, then
Total: 26.80 €— your program reads a real file, unpacks each line into two variables (day 20!) and accumulates a total (day 16!). It all connects. - Rename the file and rerun: read the FileNotFoundError, check the name it reports, fix it.
- Change an amount in the text file (not in the program!), rerun: the total follows. Your data now lives outside the code.
Agent tips: "read this file and sum the 2nd column": the agent writes that fast. Traps: it forgets the encoding, or assumes a separator (space vs tab) without looking at the file. Proof: paste it three real lines of the file and demand the total, which you check by hand.
Recap (10 min)
Three lines: with + open + encoding, why strip each line, FileNotFoundError and its two causes. Without notes: read a five-line file and print the longest line.
Day 25 — Writing to a file
Goal: create and modify files from a program: open modes and line breaks.
Theory (20 min)
open() takes a second argument: the mode.
"r": read — the default, yesterday's;"w": write — creates the file if missing, and erases its content if it exists. Today's trap: opening in "w" a file you meant to keep empties it instantly;"a": append — writes at the end without touching what exists.
with open("journal.txt", "w", encoding="utf-8") as f:
f.write("First line\n")
f.write("Second line\n")
Two differences with print: f.write() does not add a line break — you must write the \n yourself (that is the invisible "new line" character); and write only accepts strings: convert numbers with an f-string or str().
Mode "a" is perfect for a journal:
with open("journal.txt", "a", encoding="utf-8") as f:
f.write("One more line\n")
Run this program three times: the file grows by three lines. With "w", it would still hold only one. Remember the rule: "w" starts over, "a" continues the story.
What does open("x.txt", "w") do if x.txt already exists with content?
- It appends at the end
- It empties the file before writing
- It refuses and raises an error
Answer
Mode w overwrites: the previous content is lost. To append: mode "a".
Why does f.write(42) crash?
- write only accepts strings: write f.write("42") or f.write(f"{n}")
- The "w" mode is missing
- The \n is missing
Answer
TypeError: convert to text first.
Practice (30 min)
- Create
memo.py:
note = input("Your note for today? ")
with open("memo.txt", "a", encoding="utf-8") as f:
f.write(note + "\n")
print("Noted.")
- Run it three times with three different notes. Open
memo.txtin the editor: success criterion, the three notes are there, one per line. - Change "a" to "w", run twice: only the last note remains. That is the difference, seen with your own eyes. Put "a" back.
- Add reading: a second block
with open("memo.txt", encoding="utf-8") as f:that prints all the notes after the addition. - Write a script that copies yesterday's
expenses.txtintoexpenses-with-tax.txt, multiplying each amount by 1.2 (read + write: the complete exercise).
Agent tips: "append a line to this file": the agent rightly uses "a". Trap: when it "improves" a script, it may swap an "a" for a "w" without telling you — and your data vanishes on the next run. Proof: run twice and count the file's lines.
Recap (10 min)
Three lines: the three modes, the explicit \n, write only takes text. Without notes: a program that asks for three first names and writes them to a file, one per line.
Day 26 — try / except: catching errors
Goal: stop the program from crashing on a predictable error, and react cleanly.
Theory (20 min)
You can read errors; now learn to catch them. Some errors are not bugs: they are predictable — the user types "abc" when you expect a number, the file does not exist yet on first run. The try / except block says: "try this; if that error happens, do this instead of crashing":
entry = input("A number? ")
try:
n = int(entry)
print(f"Double: {n * 2}")
except ValueError:
print(f"'{entry}' is not a number.")
Python runs the try block; if a ValueError occurs inside, it jumps straight to the matching except block, and the program continues instead of stopping. The rest of the program after the try/except runs normally.
Two hygiene rules, valid for your whole coding life:
- Catch the precise error (ValueError, FileNotFoundError…), never a bare
except:that swallows everything — it would also hide your real bugs, and a program that hides its bugs is worse than a program that crashes. - Keep the try block small: only the line or lines that can raise the expected error. The bigger it is, the less you know which line triggered the except.
Combined with while, try/except gives robust input: ask again until a valid value comes.
Why avoid a bare except: with no error name?
- It is slower
- It also masks unforeseen bugs, which become impossible to find
- Python forbids it
Answer
A bare except swallows everything, including your typos: the program continues with wrong results instead of crashing.
int("twelve") inside a try: which except catches it?
- except TypeError
- except NameError
- except ValueError
Answer
The value has the right type (string) but cannot be converted: ValueError.
Practice (30 min)
- Retype the lesson's example into
robust.py, test with 21 then with "abc": both cases answer without crashing. - Forced input:
while True:
entry = input("Your age? ")
try:
age = int(entry)
break
except ValueError:
print("In digits, please.")
print(f"Thanks, {age} years old.")
- Success criterion: type "abc", then "12.5", then "25": the program asks again politely as long as it is not an integer, then thanks you. No error visible, ever.
- Think back to day 24's file: add
try: ... except FileNotFoundError:around the open() to print "create expenses.txt first" instead of crashing.
Agent tips: "make this input robust": the agent writes the while/try/except correctly. Trap: it sometimes puts the break in the wrong place and loops forever, or catches too broadly. Proof: test three invalid entries in a row, then a valid one.
Recap (10 min)
Three lines: what try/except does, why name the error, why the try block stays small. Without notes: ask for a float until you get one.
Day 27 — Parsing a CSV by hand
Goal: read a CSV file line by line, split the fields, and produce a numbered summary.
Theory (20 min)
The CSV format (comma-separated values) is the most common exchange format in the world: a table in plain text, one line per record, columns separated by commas (or semicolons in some locales). Excel, LibreOffice, your bank all export CSV:
date,label,amount
2026-10-01,groceries,-42.50
2026-10-02,salary,2100.00
2026-10-03,cinema,-9.50
The first line is the header: it names the columns, and is not treated as data. To skip it, read the first line before the loop with next(f).
Everything you need, you already have: the line-by-line loop (day 24), strip and split (day 19), unpacking (day 20), float conversion (day 6), the accumulator (day 16) and the counting pattern (day 22). Let us assemble:
total = 0.0
with open("account.csv", encoding="utf-8") as f:
next(f)
for line in f:
date, label, amount = line.strip().split(",")
total = total + float(amount)
print(f"Balance: {total:.2f} €")
An honest limit of hand-splitting: if a field itself contains a comma between quotes ("restaurant, Le Petit",-23.00), split(",") cuts in the wrong place. Python's real csv module handles that case — that will be day 50. For your own files without internal commas, the manual method is enough.
Why next(f) before the loop?
- To read and skip the header line
- To open the file
- To count the lines
Answer
The header names the columns; next() consumes it so the loop only sees data.
A case where split(",") fails on a CSV?
- When the file is utf-8
- When a field contains a comma inside quotes
- When there are more than three columns
Answer
split cuts at EVERY comma, including those inside a field. The csv module (day 50) handles quotes.
Practice (30 min)
- Create
account.csvwith the lesson's content, plus three lines of your own (one positive income, two negative expenses). - Retype the lesson's program into
balance.py, run it, check the balance with a calculator. - Success criterion: the printed balance equals your hand computation to the cent.
- Add two accumulators:
expenses(sum of negative amounts) andincome(sum of positives), with an if on the amount's sign, and print all three lines. - Add a line with a comma inside a quoted label, rerun: the program shows a ValueError or a wrong balance. On purpose: you have just hit the limit of manual splitting. Remove the line, note the limit in the notebook.
Agent tips: "summarise this CSV by column" is the archetypal agent task. Traps: assumed separator (, vs ;), forgotten header, amounts read as text. Proof: demand three numbers (income, expenses, balance) and check them by hand on five lines.
Recap (10 min)
Three lines: the structure of a CSV, next() for the header, the limit of manual split. Without notes: a name,age CSV of four lines, a program that prints the average age.
Day 28 — Module 2 review: the address book
Goal: assemble loops, lists, dicts, files and try/except into a complete two-action program.
Theory (20 min)
Nothing new: we assemble, and we take the time to design before coding. Designing a small program comes down to three questions, written in the notebook before the first line of code:
- What data? Here: contacts (name, phone). Structure: a list of "name;phone" strings in a file, and in memory a list of unpacked tuples.
- What actions? Two: "add a contact" and "list the contacts".
- What skeleton? A
while Trueloop that shows a menu (day 15), an if/elif on the choice, files for persistence (days 24-25), try/except for input (day 26).
A word on separators: we pick ; rather than , for contacts, because names may contain commas ("Smith, Jo") but rarely semicolons. Choosing your file format is already programming.
Practice (30 min)
The review program, to write alone (the skeleton is given, fill in nothing — it is complete):
FILE = "contacts.txt"
def show_menu():
print("1. Add a contact")
print("2. List contacts")
print("3. Quit")
while True:
show_menu()
choice = input("Choice? ")
if choice == "1":
name = input("Name? ").strip()
phone = input("Phone? ").strip()
with open(FILE, "a", encoding="utf-8") as f:
f.write(f"{name};{phone}\n")
print("Added.")
elif choice == "2":
try:
with open(FILE, encoding="utf-8") as f:
for line in f:
name, phone = line.strip().split(";")
print(f"{name}: {phone}")
except FileNotFoundError:
print("No contacts yet.")
elif choice == "3":
break
else:
print("Unknown choice.")
The line def show_menu(): is a preview of day 29: a named block you call by its name. Type it as it is, it works.
- Success criterion: add three contacts, list them, quit, rerun the program, list again: the three contacts survived the program's stop — that is persistence, this module's real novelty.
- Test "list" after deleting contacts.txt: the polite message prints instead of a crash.
- Improvement: refuse an empty name (if name == "" → message, no write).
Agent tips: an agent produces this whole program at once if you describe the three design questions. Do it by hand first, then compare with its version. Trap: its version may use techniques you cannot yet proofread (json, classes). Proof: you must be able to explain every line, otherwise it is not your tool.
Recap (10 min)
Three lines: the three design questions, why ";" here, what persistence is. Module 3 tomorrow: functions, for real.
Found this useful? Tell us what to measure next.
Comments
No account, no tracker: a comment is read by a human before it appears.