To-Do List CLI App
📝 To-Do List CLI App
py3-8) built a task tracker using classes, @dataclass, and a dedicated storage abstraction. This chapter builds something genuinely similar — a to-do list that persists across runs — using nothing but plain functions and a list of dicts, proving real persistence doesn't require OOP at all.
🎮 What We're Building
A command-line to-do list: add tasks, list them, mark them done, and have the list still be there the next time you run the program — all stored in a plain JSON file.
Step 1: Representing a Task as a Dict
No class needed — a task is just a dict (Course 1, Chapter 7) with two keys, and the whole to-do list is a plain list of these dicts. This is the direct "no heavy OOP required" alternative to py3-8's Task dataclass — less structure, but perfectly workable for a project this size.
Step 2: Loading and Saving with JSON
pathlib and json here work exactly like Course 2, Chapters 4 and 7 — but note there's no separate Task class to reconstruct on load, unlike py3-8's Task(**item) step. json.loads() already returns plain dicts, which is exactly what this version's tasks list is made of — one less layer, at the cost of losing the type-checking and auto-generated __eq__ a dataclass would give.
Step 3: Adding, Listing, Completing Tasks
enumerate(tasks, start=1) reuses Course 1, Chapter 4's numbered-list pattern for a human-friendly 1-based display. complete_task mutates the dict directly through its list position — tasks[index]["done"] = True — since tasks.append() is passed the same list object every function receives (Course 1's mutable-list-aliasing behavior, here working in the app's favor).
complete_task identifies a task purely by its position in the list. If tasks are ever deleted (see this chapter's extension ideas) or reordered, a previously-noted "task #3" can silently become a different task. py3-8's dataclass version sidestepped this entirely with a persistent id field that never changes — worth adding here too if the list ever needs deletion or reordering support.
Step 4: The Menu Loop
Same menu-loop shape as Chapters 1 and 2. save_tasks(tasks) is called after every mutation (add, complete) — skip it and the change would only exist in memory for this run, lost the moment the program exits, undermining the whole point of persistence.
🏁 The Complete To-Do App
Task as a dict
No class needed — {"title": ..., "done": ...} is enough.
load_tasks / save_tasks
JSON + pathlib persistence, functions instead of a storage class.
Mutating through a shared reference
Functions modify the same tasks list every caller shares.
Save after every mutation
Skipping save_tasks() loses the change when the program exits.
Try these on your own:
- Add a
delete_task(tasks, index)function and menu option, using a list comprehension (Course 1/2) to build the filtered list. - Fix this chapter's own warn-box: give each task a persistent
id(an incrementing counter, likepy3-8'snext_idlogic) instead of relying on list position. - Add input validation with
try/except(Course 2, Chapter 3) around the task-number input. - Add a due date field and sort
list_tasks()'s output by it.
🎯 What's Next
Next chapter: Quiz Game — dictionaries, lists, and scoring logic.