Project: A To-Do App
Build a real program, start to finish.
Time to build something real. Ava wants a to do list for her shop tasks, with adding, completing, and saving them between runs. We will plan it, build it in small steps, and test each step. It uses lists, dictionaries, functions, files, and JSON, everything from the first half of this course.

- The goal: a to-do list that remembers
- Plan first, then build in small steps
- Uses functions, dicts, lists and JSON
Plan the program
Before any code, plan. Each task is a dictionary with a title, and a done flag. All tasks live in a list. We need four functions, add, complete, show, and save and load, with JSON. The main part ties them together. Small functions, each doing one job, are easy to test, and easy to change.
The core functions
Here are the core functions. Add appends a new task. Complete marks one as done, and refuses a bad number, with a clear error. Show prints a checkbox, and the title, for every task, numbered from one. We build a small list, complete one item, and show the result.
def add(tasks, title):
tasks.append({"title": title, "done": False})
def complete(tasks, n):
if not 1 <= n <= len(tasks):
raise ValueError(f"no task {n}")
tasks[n - 1]["done"] = True
def show(tasks):
for i, t in enumerate(tasks, 1):
print(i, "[x]" if t["done"] else "[ ]", t["title"])
tasks = []
add(tasks, "Water the ferns")
add(tasks, "Pack Maya's order")
complete(tasks, 1)
show(tasks)1 [x] Water the ferns 2 [ ] Pack Maya's order
Saving and loading
Now persistence. Save writes the list as JSON. Load reads it back, and returns an empty list, when the file does not exist yet, which is the first run. Using pathlib keeps it tidy. We save, then load into a new variable, to prove the data survived.
import json
from pathlib import Path
FILE = Path("tasks.json")
def save(tasks):
FILE.write_text(json.dumps(tasks, indent=2))
def load():
return json.loads(FILE.read_text()) if FILE.exists() else []
print(load())
save([{"title": "Water the ferns", "done": True}])
print(load())[]
[{'title': 'Water the ferns', 'done': True}]The interactive loop
Finally, the main loop. It shows a menu, reads a command, and calls the right function. Errors are caught, so a typing mistake never crashes the program. Here, we feed it a few commands, add, done, list, and quit, and watch the whole app work. You just built a real program.
tasks = []
while True:
cmd = input("> ").strip()
if cmd == "quit":
break
elif cmd.startswith("add "):
tasks.append({"title": cmd[4:], "done": False})
elif cmd.startswith("done "):
try:
tasks[int(cmd[5:]) - 1]["done"] = True
except (ValueError, IndexError):
print("no such task")
elif cmd == "list":
for i, t in enumerate(tasks, 1):
print(i, "[x]" if t["done"] else "[ ]", t["title"])
else:
print("commands: add, done, list, quit")> add Water ferns > add Pack order > done 1 > done 9 no such task > list 1 [x] Water ferns 2 [ ] Pack order > help commands: add, done, list, quit > quit
Add a remove command
Your turn. Extend the idea, with a function that removes a task by its number, and raises a ValueError for a bad number. Then test it on a small list. Ready? Here is one solution. Check the range, and delete with pop, using a zero based index.
Write remove(tasks, n) that deletes task number n (1-based), raising ValueError if invalid.
Show hint
tasks.pop(n - 1)
Show solution
def remove(tasks, n):
if not 1 <= n <= len(tasks):
raise ValueError(f"no task {n}")
return tasks.pop(n - 1)
tasks = ["a", "b", "c"]
print(remove(tasks, 2))
print(tasks)
try:
remove(tasks, 9)
except ValueError as e:
print(e)b ['a', 'c'] no task 9
Summary
Congratulations. You planned a program, built small functions, saved data with JSON, and wrapped everything in an interactive loop. Next, a second project, an expense tracker, with files, dictionaries and reports.
Key takeaways- Plan, then small functions
- JSON for persistence
- Catch errors in the main loop