Python Tutorial
Python Project: To-Do List
Keep tasks in a list: add, show, complete, and delete items without losing the rest of the list.
What you are building
You keep a list of task titles as strings. New work is appended. Finishing a task moves it to a second list with pop. Deleting a task also uses pop, but the title is not kept. At the end you print what is still open.
Paste the examples into /try and run them. That editor is Python. It will not render HTML and it will not compile C or C++. The demo never calls input(), so the full script prints in one click.
Skills used
- A list of strings for open tasks
appendto add at the endpop(index)to remove by positionenumerateso printed lines show a number the user can quote- A second list for completed titles
Add and show
Start empty. Append three titles. Print each with its index. Indexes start at 0, the same numberpop will use later.
Example
todo = []
todo.append("Write the report")
todo.append("Call the lab")
todo.append("Buy milk")
for i, title in enumerate(todo):
print(i, title)
print("Count:", len(todo))append never overwrites an earlier item. That is the point of a list: the old titles stay, the new one lands at the end.
Complete and delete by index
Completing is a move: take the title out of todo and put it on done. Deleting is a drop: take it out and ignore it. Both use pop. After a pop, later indexes shift down by one.
Example
todo = ["Write the report", "Call the lab", "Buy milk"]
done = []
title = todo.pop(1)
done.append(title)
print("Completed:", title)
print("Open:", todo)
print("Done:", done)
removed = todo.pop(1)
print("Deleted:", removed)
print("Open now:", todo)| Action | List change |
|---|---|
| Add | todo.append(title) |
| Complete | done.append(todo.pop(i)) |
| Delete | todo.pop(i) and discard |
| Show remaining | print every string still in todo |
After you pop index 1, the old index 2 becomes index 1. Always print the list again before the next pop. Guessing a stale number is the usual way this program loses the wrong task.
Complete program
Helpers wrap add, show, complete, and delete. A short script of calls stands in for a menu. Run it at/try and read the remaining list at the bottom.
Example
def show(label, items):
print(label)
if not items:
print(" (none)")
return
for i, title in enumerate(items):
print(f" {i}: {title}")
def add(todo, title):
todo.append(title)
print("Added:", title)
def complete(todo, done, index):
if index < 0 or index >= len(todo):
print("No task at index", index)
return
title = todo.pop(index)
done.append(title)
print("Completed:", title)
def delete(todo, index):
if index < 0 or index >= len(todo):
print("No task at index", index)
return
title = todo.pop(index)
print("Deleted:", title)
todo = []
done = []
add(todo, "Write the report")
add(todo, "Call the lab")
add(todo, "Buy milk")
print()
show("Open tasks", todo)
print()
complete(todo, done, 1)
print()
show("Open tasks", todo)
print()
delete(todo, 1)
print()
show("Open tasks", todo)
show("Completed", done)
print()
print("Remaining:")
for title in todo:
print("-", title)Walk the log: three adds, complete index 1 (Call the lab), then delete the new index 1 (Buy milk). Remaining is Write the report. Completed holds Call the lab.
Common mistakes
- Using
remove(title)when two tasks share a name.removedrops the first match only. Indexes are exact. - Calling
pop()with no argument. That removes the last item, not the one you numbered. - Printing 1-based numbers and then popping that same number. If you show 1, 2, 3, you must
pop(n - 1). - Completing with
todo[index] = "done". The title is then lost and you cannot list finished work. - Forgetting the bounds check.
pop(5)on a short list raisesIndexErrorand the rest of the script never runs.
How to extend / Practice tasks
Three changes you can make in the complete program.
- Add a fourth task
"Email the draft"and complete index 0 first. Confirm the remaining order is Call the lab, Buy milk, Email the draft. - Print a 1-based menu (
1:,2:) but keeppopzero-based. Convert withindex = choice - 1incompleteanddelete. - Refuse empty titles: if
title.strip()is empty, print a message and skipappend. Test it by callingadd(todo, " ").
📘 Real-World Deep Dive
A to-do app is the smallest project that touches persistence: you add, list, complete, and delete items, then save them so they survive a restart. That add/list/save loop is the core of almost every CRUD app you'll ever build.
What to build
An in-memory task list with add / complete / remaining operations, structured so swapping in JSON-file storage later is a one-function change.
Real-Life Example
from dataclasses import dataclass, field
@dataclass
class TodoList:
tasks: list[dict] = field(default_factory=list)
def add(self, text: str) -> None:
self.tasks.append({"text": text, "done": False})
def complete(self, i: int) -> None:
self.tasks[i]["done"] = True
def remaining(self) -> list[str]:
return [t["text"] for t in self.tasks if not t["done"]]
todo = TodoList()
todo.add("write tests"); todo.add("ship it"); todo.add("sleep")
todo.complete(0)
print("left:", todo.remaining())Keeping storage as plain dicts/lists means saving to JSON later is literally json.dump(todo.tasks, f).
Expected Output
left: ['ship it', 'sleep']Common mistakes
- A shared mutable default (
tasks: list = []) leaks state across every instance — usefield(default_factory=list). - Indexing by position breaks after a delete shifts everything. Give each task a stable
idonce the list can be edited. - Writing the file on every keystroke is slow and can corrupt on a crash mid-write — save on change and write to a temp file, then rename.
🚀 Performance & Best Practices
- For a personal list, a JSON file is perfect; reach for SQLite only when you need queries or thousands of items.
- Filtering with a comprehension (
remaining) is clear and fast enough for any human-sized list. - Keep the data model (TodoList) separate from the UI so a CLI, web, or GUI front-end can all reuse it.
🧪 Try It Yourself
- Add
save(path)/load(path)withjsonso tasks survive a restart. - Add priorities and sort
remaining()by them. - Give each task an id and make
complete/deletework by id, not index.