Todo List TUI in Python

This one is short, useful, and very beginner-friendly.

Read the mental model first

Download source file

Quick Run (from repo root)

1cd Site/static/code/examples/python
2python todo_cli.py

You get a complete todo CLI with JSON persistence.

Features

  • add/edit/delete tasks
  • mark tasks complete
  • filter done vs pending
  • persist to JSON file

Code (Pythonic + Short)

  1from dataclasses import asdict, dataclass
  2from pathlib import Path
  3import json
  4
  5
  6SAVE_FILE = Path("todo-python.json")
  7
  8
  9@dataclass
 10class Task:
 11    id: int
 12    text: str
 13    done: bool = False
 14
 15
 16def load_tasks() -> list[Task]:
 17    if not SAVE_FILE.exists():
 18        return []
 19    raw = json.loads(SAVE_FILE.read_text())
 20    return [Task(**item) for item in raw]
 21
 22
 23def save_tasks(tasks: list[Task]) -> None:
 24    SAVE_FILE.write_text(json.dumps([asdict(t) for t in tasks], indent=2))
 25
 26
 27def next_id(tasks: list[Task]) -> int:
 28    return max((task.id for task in tasks), default=0) + 1
 29
 30
 31def print_tasks(tasks: list[Task], mode: str = "all") -> None:
 32    if mode == "done":
 33        view = [t for t in tasks if t.done]
 34    elif mode == "open":
 35        view = [t for t in tasks if not t.done]
 36    else:
 37        view = tasks
 38
 39    if not view:
 40        print("No matching tasks.")
 41        return
 42
 43    for task in view:
 44        box = "[x]" if task.done else "[ ]"
 45        print(f"{task.id}. {box} {task.text}")
 46
 47
 48def find_task(tasks: list[Task], task_id: int) -> Task | None:
 49    return next((task for task in tasks if task.id == task_id), None)
 50
 51
 52def read_task_id(prompt: str) -> int:
 53    raw = input(prompt).strip()
 54    return int(raw) if raw.isdigit() else -1
 55
 56
 57def main():
 58    tasks = load_tasks()
 59    print("Commands: add, list, done, edit, delete, filter, quit")
 60
 61    while True:
 62        command = input("> ").strip().lower()
 63
 64        if command == "add":
 65            text = input("Task: ").strip()
 66            if text:
 67                tasks.append(Task(id=next_id(tasks), text=text))
 68                save_tasks(tasks)
 69                print("Added.")
 70
 71        elif command == "list":
 72            print_tasks(tasks)
 73
 74        elif command == "done":
 75            task_id = read_task_id("Task id: ")
 76            task = find_task(tasks, task_id)
 77            if task:
 78                task.done = True
 79                save_tasks(tasks)
 80                print("Marked done.")
 81            else:
 82                print("Task not found.")
 83
 84        elif command == "edit":
 85            task_id = read_task_id("Task id: ")
 86            task = find_task(tasks, task_id)
 87            if task:
 88                new_text = input("New text: ").strip()
 89                if new_text:
 90                    task.text = new_text
 91                    save_tasks(tasks)
 92                    print("Updated.")
 93            else:
 94                print("Task not found.")
 95
 96        elif command == "delete":
 97            task_id = read_task_id("Task id: ")
 98            before = len(tasks)
 99            tasks = [task for task in tasks if task.id != task_id]
100            if len(tasks) != before:
101                save_tasks(tasks)
102                print("Deleted.")
103            else:
104                print("Task not found.")
105
106        elif command == "filter":
107            mode = input("Mode (all/open/done): ").strip().lower()
108            print_tasks(tasks, mode if mode in {"all", "open", "done"} else "all")
109
110        elif command == "quit":
111            save_tasks(tasks)
112            print(f"Saved to {SAVE_FILE}")
113            break
114
115        else:
116            print("Use: add, list, done, edit, delete, filter, quit")
117
118
119if __name__ == "__main__":
120    main()

Run It

1python todo_cli.py

Why this is pythonic

  • dataclass models each task clearly.
  • Path and json keep persistence simple and standard-library only.
  • List comprehensions and generator expressions keep code compact and readable.