To-Do Manager with File Persistence
A real CRUD app in miniature: add and complete tasks, persist them to disk so nothing is lost on restart, and report progress — the heart of Todoist, Trello and every task app.
How it works in the real world
Every task app is CRUD — Create, Read, Update, Delete — plus persistence:
- State — tasks live in memory as a list of dicts (like a mini database table).
- Operations — add and complete are functions that modify state.
- Persistence — state is serialised to a file and reloaded on startup; without this, a restart wipes everything.
- Reporting — aggregate state into stats users care about.
Swap the file for SQLite and the print for HTML and you literally have a web app — that's the whole secret.
Create & read
Implement add_task(title) appending {"id": next_id, "title": title, "done": False} (ids start at 1). Add the three tasks, then print each as 1. [ ] buy milk.
id is len(tasks) + 1. The checkbox: 'x' if t["done"] else ' ' inside an f-string.
tasks = []
def add_task(title):
tasks.append({"id": len(tasks) + 1, "title": title, "done": False})
add_task("buy milk")
add_task("walk dog")
add_task("write code")
for t in tasks:
box = "x" if t["done"] else " "
print(f"{t['id']}. [{box}] {t['title']}")
Run your code to check this step…
Update: complete a task
Implement complete_task(task_id) that marks the matching task done. Complete task 2, then print the list — task 2 shows [x].
Loop the tasks; when t["id"] == task_id, set t["done"] = True.
tasks = [
{"id": 1, "title": "buy milk", "done": False},
{"id": 2, "title": "walk dog", "done": False},
{"id": 3, "title": "write code", "done": False},
]
def complete_task(task_id):
for t in tasks:
if t["id"] == task_id:
t["done"] = True
complete_task(2)
for t in tasks:
box = "x" if t["done"] else " "
print(f"{t['id']}. [{box}] {t['title']}")
Run your code to check this step…
Persistence: save & load
Save each task to todo.txt as id|done|title (done as 0/1), then load the file back into a new list and print the count and the second task's title: 3 walk dog.
Write f"{t['id']}|{int(t['done'])}|{t['title']}\n". Read with line.strip().split("|") and rebuild each dict.
tasks = [
{"id": 1, "title": "buy milk", "done": False},
{"id": 2, "title": "walk dog", "done": True},
{"id": 3, "title": "write code", "done": False},
]
with open("todo.txt", "w") as f:
for t in tasks:
f.write(f"{t['id']}|{int(t['done'])}|{t['title']}\n")
loaded = []
with open("todo.txt") as f:
for line in f:
task_id, done, title = line.strip().split("|")
loaded.append({"id": int(task_id), "title": title, "done": done == "1"})
print(len(loaded), loaded[1]["title"])
Run your code to check this step…
The progress report
Print one summary line: 3 tasks: 1 done, 2 pending (33% complete) — percentage rounded to a whole number.
done = sum(1 for t in tasks if t["done"]) — then round(done / len(tasks) * 100).
tasks = [
{"id": 1, "title": "buy milk", "done": False},
{"id": 2, "title": "walk dog", "done": True},
{"id": 3, "title": "write code", "done": False},
]
done = sum(1 for t in tasks if t["done"])
pending = len(tasks) - done
pct = round(done / len(tasks) * 100)
print(f"{len(tasks)} tasks: {done} done, {pending} pending ({pct}% complete)")
Run your code to check this step…
Project shipped!
You just built a complete, working to-do manager with file persistence — the same architecture running in production software. Pick your next build →