Tasks in Memory
Before a to-do app can save anything or take commands, it needs one thing: a way to hold a task in the program's memory. Get the shape of the data right and everything after this gets shorter. So that's where we start - no files, no commands yet, only tasks living in a list while the program runs.
What is a task, really?
A task isn't one value. It has a few parts: the text of the thing to do, whether it's finished, and an id so we can point at it later ("mark task 2 done"). When you have a bundle of named values like that, a Python dictionary is the natural fit.
Here's one task as a dict:
=
Run that. You get the whole dict, then two values pulled out by name. The keys - id, text, done - are how we reach inside. That's the entire data model for one task. No class, no library. A dict is plenty.
Many tasks: a list of dicts
One task is a dict. A to-do list is, fittingly, a Python list of those dicts:
=
A list of dictionaries is one of the most common shapes in all of Python. Rows from a database, items in a shopping cart, results from an API - they almost always arrive looking like this. Learn it here and you'll recognize it everywhere.
Adding a task
Right now we typed the tasks by hand. The app needs to add them on demand. We'll write a function that takes the current list, the new text, and appends a fresh dict to the end.
The one wrinkle is the id. Each task needs a number nobody else has. The reliable trick: look at the biggest id already in the list and add one. If the list is empty, start at 1.
= + 1
= 1
=
Walk through it. We start with an empty list. The first add_task sees no tasks, so new_id is 1. The next sees a max id of 1, so it picks 2. Then 3. The list grew from nothing to three tasks, each with its own id, and we never had to track a counter ourselves.
Why max instead of len(tasks) + 1? Because later we'll delete tasks. If you delete task 2 from a list of three, len is now 2 - and len + 1 would hand the next task an id of 3, which already exists. Reading the actual max id keeps every id unique no matter what you've removed. It's a small choice now that saves a real bug later.
Listing what you have
Adding is half of it. The other half is showing the list back in a way a human wants to read. Let's make a list_tasks function that prints each task with its id and a marker for whether it's done.
= + 1
return
=
=
= True # pretend we finished the first one
A couple of things to notice. We tightened add_task using max(..., default=0) - the default kicks in when the list is empty, so the if/else disappears and the function is one line of logic. Same behavior, less code.
In list_tasks, the mark line is a small conditional: "x" if the task is done, a space if not. The f"..." string drops the values straight into the text. The result reads like an actual checklist:
[x] 1. buy milk
[ ] 2. call the bank
That bracket-and-number format is something you can scan in a real terminal. We set tasks[0]["done"] = True by hand here only to show the marker working - in Phase 3 we'll write a proper function for it.
Where we are
You now have the spine of the app: a task is a dict, the list is a list of dicts, add_task grows it, and list_tasks shows it. Everything from here builds on this shape.
The catch - and you may have felt it - is that the moment the program ends, the list vanishes. Run it again and you're back to empty. That's the next problem to solve: making your tasks stick around. On to saving them to a file.