Project: a flashcard trainer
Cards from a file, review boxes, and progress saved between runs.
Create a free account Try part of it here
Why it matters
A program that forgets everything when it stops is a toy. The moment it saves its state and picks up where it left off, it becomes a tool, and two new problems arrive: the first run, when there's nothing saved yet, and the bad day, when the saved file is broken. Every app, from a to-do list to an agent's memory, has to handle both. This project also explains how the academy's own review cards decide what to show you.
What you'll be able to do
- Load data from a CSV file into objects, and save progress as JSON
- Handle a first run with no saved file, and a saved file that's damaged
- Schedule reviews with boxes, so known cards come up less often
- Combine classes, files, errors and input() into one program that remembers
What it covers
- The brief
- Boxes: how spaced review works
- Checkpoint 1: cards as objects
- Move a card between boxes
- Checkpoint 2: saving progress as JSON
- Checkpoint 3: the first run, and the bad day
- Worked example: one short session
- Go further
The brief
You'll build a flashcard trainer for the academy's own vocabulary: variable, loop, function and so on. Each card has a front (the term) and a back (what it means).
- The cards start in a CSV file, one card per row.
- In a session, the trainer shows each card that's due. You say whether you knew it.
- Cards you know come up less and less often. Cards you miss come back every session.
- Progress is saved to a JSON file, so the next session continues where this one stopped.
- The first time, there's no progress file: start from the CSV. If the progress file is damaged, say so, and start from the CSV again rather than crashing.
That's four lessons in one program: classes, files, JSON and errors. You'll build it in pieces and check each one before joining them.
Boxes: how spaced review works
Reviewing everything every time wastes effort on what you already know. A simple, well-tested system uses numbered boxes, named after Sebastian Leitner, who described it in the 1970s:
- Every card starts in box 1, which is reviewed every session.
- Know a card, and it moves up a box. Box 2 is reviewed every second session, box 3 every fourth.
- Miss a card, and it goes back to box 1, wherever it was.
So a card you keep getting right fades into the background, and a card you struggle with stays in front of you. The academy's review deck works on the same idea, with more careful timing: it uses days rather than sessions, and how hard you found a card, not just right or wrong.
In code, "every second session" is a remainder test, the same % you met with numbers:
EVERY = {1: 1, 2: 2, 3: 4}
for session in range(1, 9):
due_boxes = [box for box, every in EVERY.items() if session % every == 0]
print(session, due_boxes)Box 1 is due every session, box 2 on even sessions, and box 3 on sessions 4 and 8.
box = 2 box = min(box + 1, 3) # known print(box) box = 1 # missed print(box)
Show the answer
3 then 1
Knowing it moves it up to box 3, but a miss always sends a card back to box 1, so it gets reviewed every session again.
Checkpoint 1: cards as objects
Each card is a small object with a box number. csv.DictReader reads the file one row at a time, as dictionaries keyed by the header row:
import csv with open("cards.csv", "w") as f: f.write("front,back\nvariable,A name that holds a value\nloop,Code that repeats\n") class Card: def __init__(self, front, back, box=1): self.front = front self.back = back self.box = box def __repr__(self): return f"Card({self.front!r}, box {self.box})" def load_cards(path): with open(path) as f: return [Card(row["front"], row["back"]) for row in csv.DictReader(f)] print(load_cards("cards.csv"))
The first lines write the CSV, so the example has a file to read. In the real program it would already exist, perhaps exported from a spreadsheet. __repr__ gives each card a readable printout, which makes the list on the last line easy to check.
Move a card between boxes
Write mark(card, known). A known card moves up one box, but never above box 3. A missed card goes back to box 1.
class Card: def __init__(self, front, back, box=1): self.front = front self.back = back self.box = box def mark(card, known): pass
Checkpoint 2: saving progress as JSON
The CSV holds the cards; progress also needs each card's box. JSON fits, because a list of dictionaries turns into JSON text and back without any extra code:
import json progress = [ {"front": "variable", "back": "A name that holds a value", "box": 2}, {"front": "loop", "back": "Code that repeats", "box": 1}, ] with open("progress.json", "w") as f: json.dump(progress, f, indent=2) with open("progress.json") as f: back_again = json.load(f) print(back_again == progress, back_again[0]["box"])
json.dump writes the data to an open file, and json.load reads it back. indent=2 spreads it over several lines, so a person can open the file and read it. The comparison prints True: what you save is exactly what you get back.
A saved progress file, as the program reads it back. Click a value to see the Python that reaches it, such as progress[2]["box"].
Checkpoint 3: the first run, and the bad day
Loading progress can go wrong in two ways, and they deserve different answers:
- There's no file yet.
openraisesFileNotFoundError. That isn't a problem at all; it's the first run. Start from the CSV, quietly. - The file is damaged: half-written, or edited by hand.
json.loadraisesjson.JSONDecodeError. Say so, so the person knows their progress was lost, then start from the CSV.
import json def load_progress(path): try: with open(path) as f: return json.load(f) except FileNotFoundError: return None except json.JSONDecodeError: print(f"Couldn't read {path}, so starting again.") return None with open("broken.json", "w") as f: f.write('[{"front": "loop", "box": ') print(load_progress("never-saved.json")) print(load_progress("broken.json"))
Both cases return None, meaning "no usable progress", and the caller then builds a fresh deck from the CSV. Each except names the exact error it expects. A bare except: would also hide real bugs, like a typo in a variable name, which you'd much rather see.
except:?Show the answer
It would also hide unrelated bugs, and you couldn't tell a first run from a damaged file
A bare except catches everything, including mistakes you need to see. Naming each error lets you treat a missing file and a damaged one differently.
Worked example: one short session
Three cards, all due in session 1. You answer y for a card you knew and n for one you didn't. The boxes move accordingly.
class Card: def __init__(self, front, back, box=1): self.front, self.back, self.box = front, back, box cards = [Card("variable", "A name that holds a value"), Card("loop", "Code that repeats"), Card("function", "A named, reusable piece of code")] score = 0 for card in cards: answer = input(f"{card.front}? (y = knew it, n = didn't) ").strip().lower() if answer == "y": card.box = min(card.box + 1, 3) score = score + 1 else: card.box = 1 print(" It means:", card.back) print("Score:", score, "of", len(cards)) print([(c.front, c.box) for c in cards])
The loop shows each front and asks you to judge yourself, which is how most flashcard apps work. Typing the answer out would make checking hard: "a name for a value" and "a name that holds a value" mean the same thing. A missed card also shows its back, so the miss teaches you something. Anything other than y counts as a miss, which keeps the loop simple. The last line shows variable and function moved to box 2, and loop staying in box 1.
Go further
The trainer is complete. Some ways to extend it:
- Shuffle the due cards, so the order changes each session.
- Show a summary at the end of each session: how many cards are in each box.
- Let the cards come from the practice API instead of a file, with one function that fetches them. The rest of the program shouldn't need to change.
- Save after every answer, not just at the end, so closing the program halfway loses nothing.
The last idea is about durability: the less work a crash can lose, the better the program. That idea comes back, at a much bigger scale, when you store data in databases and run services that must not lose anything.
Practice what you just read
- 5 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
The progress file doesn't exist yet. What should the trainer do?