PyPython · Lesson 8 of 14

Mini Project: CLI Todo App

Let's build something real: a command-line todo app that saves your tasks to a file. Everything you've learned, put together.

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

Python
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()

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 module — 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
◆ Note
Next steps: try adding a "due date" field, or rewrite the file storage to use SQLite instead of JSON (hint: look at the sqlite3 module in the standard library).