Lesson 8 of 34 · 0 of 1 exercises solved

Mini Project: CLI Todo App

In this project you build a command-line todo app that saves tasks to a file. It brings together everything from the earlier lessons.

We'll build a todo app that persists tasks to a file. It will support adding, listing, completing, and deleting tasks. This uses file I/O, JSON, , and all the other concepts from previous lessons.

import json
import os
import sys
from datetime import datetime

TODO_FILE = "todos.json"

def load_todos():
    if not os.path.exists(TODO_FILE):
        return []
    with open(TODO_FILE, "r") as f:
        return json.load(f)

def save_todos(todos):
    with open(TODO_FILE, "w") as f:
        json.dump(todos, f, indent=2)

def add_todo(text):
    todos = load_todos()
    todo = {
        "id": len(todos) + 1,
        "text": text,
        "done": False,
        "created": datetime.now().isoformat()
    }
    todos.append(todo)
    save_todos(todos)
    print(f"Added: {text}")

def list_todos():
    todos = load_todos()
    if not todos:
        print("No todos yet. Add some!")
        return
    for todo in todos:
        status = "✓" if todo["done"] else "○"
        print(f"[{todo['id']}] {status} {todo['text']}")

def complete_todo(todo_id):
    todos = load_todos()
    for todo in todos:
        if todo["id"] == todo_id:
            todo["done"] = True
            save_todos(todos)
            print(f"Completed: {todo['text']}")
            return
    print(f"No todo with id {todo_id}")

def delete_todo(todo_id):
    todos = load_todos()
    todos = [t for t in todos if t["id"] != todo_id]
    save_todos(todos)
    print(f"Deleted todo {todo_id}")

def main():
    if len(sys.argv) < 2:
        print("Usage: python todo.py [add|list|done|delete] [args]")
        return

    command = sys.argv[1]

    if command == "add" and len(sys.argv) > 2:
        add_todo(" ".join(sys.argv[2:]))
    elif command == "list":
        list_todos()
    elif command == "done" and len(sys.argv) > 2:
        complete_todo(int(sys.argv[2]))
    elif command == "delete" and len(sys.argv) > 2:
        delete_todo(int(sys.argv[2]))
    else:
        print("Unknown command or missing arguments")

if __name__ == "__main__":
    main()
Python (loads on first run)Ctrl+Enter runs

Save this as todo.py and try it out. The if __name__ == "__main__": pattern means the main() function only runs when you execute the file directly, not when it's imported as a — a crucial Python convention.

Bash
python todo.py add "Buy groceries"
python todo.py add "Write code"
python todo.py list
python todo.py done 1
python todo.py list
python todo.py delete 2

Common question

Should a beginner build a whole program before mastering the parts?

A project is where the parts come together. Functions get callers, data structures get data, handling gets errors, and file I/O gets a reason to exist. The to-do app is small enough to finish in one sitting but shaped like real software: a that reads commands, hands them to functions, and keeps state on disk between runs.

Build it in . First print a hard-coded ; then add tasks in ; then save and load; then complete and delete. Run it after every slice. When it works, extend it in a direction you care about (due dates, priorities, colours), because the extension you actually want is the one you will finish.

Source: The C Programming Language by Kernighan and Ritchie ends almost every section with exercises that ask you to write a small working program.

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • You should learn every feature first and combine them later; building early just locks in bad habits.

  • A to-do is too simple to teach anything.

  • Storing tasks in a JSON file is unprofessional; real programs use .

Practice

The todo app stores tasks as dictionaries. Write two helpers for it: complete_task(tasks, index) marks the task at that 1-based position as done and returns True, or returns False if the index is out of range; pending(tasks) returns a list of the titles that are not yet done.

Exercise

Todo helpers

3 checks
tasks = [
    {"title": "Read lesson 8", "done": False},
    {"title": "Write FizzBuzz", "done": True},
    {"title": "Push to GitHub", "done": False},
]


def complete_task(tasks, index):
    ...


def pending(tasks):
    ...


complete_task(tasks, 1)
print(pending(tasks))
 
Python (loads on first run)Ctrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.
Finished reading?1 exercise still open. You can come back to it later.

Tip: use the ← and → keys to move between lessons.