Build a Command-Line Todo App with Python in 20 Minutes

You know that feeling when you’re knee-deep in code, juggling three tasks, and you realize you’ve forgotten which one was urgent? I’ve been there more times than I care to admit.

Sure, there are a hundred todo apps out there. But sometimes you don’t need a cloud-synced, AI-powered task manager. Sometimes you just need something that works in your terminal, doesn’t require an internet connection, and you actually understand.

That’s what we’re building today: a command-line todo app in Python. It takes about 20 minutes, and you’ll walk away with a working project that solves a real problem. If you’ve ever wanted to get comfortable with bash scripting and CLI tools, this is a great next step.

What We’re Building

Our app handles five operations:

  • Add new tasks
  • List all tasks with completion status
  • Mark tasks as done
  • Delete tasks
  • Persist data between sessions using a JSON file

Here’s the end result:

$ python todo.py add "Finish project report"
✓ Added: Finish project report

$ python todo.py list

📋 Your Todos:
--------------------------------------------------
○ 1. Finish project report
--------------------------------------------------
Total: 1 | Completed: 0

$ python todo.py done 1
✓ Completed: Finish project report

Prerequisites

  • Python 3.6+ (check with python3 --version)
  • A text editor
  • Basic Python knowledge (variables, functions, loops)
  • A terminal

No external libraries needed. We’re using only Python’s built-in modules: json, os, datetime, and sys.

Step 1: Create the Project

mkdir python-todo-app && cd python-todo-app
touch todo.py

Open todo.py in your editor. Let’s build it piece by piece.

Step 2: Imports and Constants

#!/usr/bin/env python3
import json
import os
from datetime import datetime

TODO_FILE = "todos.json"
  • json — save and load todos from a file
  • os — check if the file exists
  • datetime — timestamp each todo
  • TODO_FILE — name of our data file

Step 3: Load and Save Functions

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

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

load_todos() returns an empty list if the file doesn’t exist yet. save_todos() writes with indent=2 for human-readable formatting. The with statement auto-closes files — always use it.

Step 4: The Add Function

def add_todo(task):
    """Add a new todo item"""
    todos = load_todos()
    todo = {
        "id": len(todos) + 1,
        "task": task,
        "completed": False,
        "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    }
    todos.append(todo)
    save_todos(todos)
    print(f"✓ Added: {task}")

Each todo is a dictionary with an auto-incremented ID, the task text, a completion flag, and a timestamp.

Step 5: The List Function

def list_todos():
    """Display all todos"""
    todos = load_todos()
    if not todos:
        print("No todos yet. Add one with: python todo.py add \"Your task\"")
        return
    
    print("\n📋 Your Todos:")
    print("-" * 50)
    for todo in todos:
        status = "✓" if todo["completed"] else "○"
        task_text = todo["task"]
        if todo["completed"]:
            task_text = f"\033[9m{task_text}\033[0m"
        print(f"{status} {todo['id']}. {task_text}")
    print("-" * 50)
    completed = sum(1 for t in todos if t["completed"])
    print(f"Total: {len(todos)} | Completed: {completed}")

The \033[9m and \033[0m ANSI codes create strikethrough text for completed items. Works in most modern terminals. The sum() line uses a generator expression to count completed todos.

Step 6: Complete and Delete Functions

def complete_todo(todo_id):
    """Mark a todo as completed"""
    todos = load_todos()
    for todo in todos:
        if todo["id"] == todo_id:
            todo["completed"] = True
            save_todos(todos)
            print(f"✓ Completed: {todo['task']}")
            return
    print(f"✗ Todo #{todo_id} not found")

def delete_todo(todo_id):
    """Delete a todo"""
    todos = load_todos()
    todos = [t for t in todos if t["id"] != todo_id]
    for i, todo in enumerate(todos, 1):
        todo["id"] = i
    save_todos(todos)
    print(f"✓ Deleted todo #{todo_id}")

delete_todo() uses a list comprehension to filter out the target, then re-numbers IDs so they stay sequential.

Step 7: The Main Function (CLI Parser)

def main():
    import sys
    
    if len(sys.argv) < 2:
        show_help()
        return
    
    command = sys.argv[1].lower()
    
    if command == "add":
        if len(sys.argv) < 3:
            print("✗ Please provide a task description")
            return
        task = " ".join(sys.argv[2:])
        add_todo(task)
    elif command == "list":
        list_todos()
    elif command == "done":
        if len(sys.argv) < 3:
            print("✗ Please provide a todo ID")
            return
        try:
            complete_todo(int(sys.argv[2]))
        except ValueError:
            print("✗ Invalid ID. Please provide a number")
    elif command == "delete":
        if len(sys.argv) < 3:
            print("✗ Please provide a todo ID")
            return
        try:
            delete_todo(int(sys.argv[2]))
        except ValueError:
            print("✗ Invalid ID. Please provide a number")
    elif command == "help":
        show_help()
    else:
        print(f"✗ Unknown command: {command}")
        show_help()

def show_help():
    print("""
📝 Python Todo App - Commands:

  python todo.py add "Task description"  - Add a new todo
  python todo.py list                    - List all todos
  python todo.py done <id>               - Mark todo as completed
  python todo.py delete <id>             - Delete a todo
  python todo.py help                    - Show this help

Examples:
  python todo.py add "Buy groceries"
  python todo.py done 1
""")

if __name__ == "__main__":
    main()

sys.argv is a list of command-line arguments. sys.argv[0] is the script name, and the rest are what you pass. The try/except blocks catch ValueError if someone passes a non-numeric ID.

Step 8: Test Everything

Save the file and run these commands:

# Add some todos
python todo.py add "Buy groceries at SM Supermarket"
python todo.py add "Finish the quarterly report"
python todo.py add "Review pull request #42"

# List them
python todo.py list

# Complete one
python todo.py done 2

# Delete one
python todo.py delete 3

# Check the result
python todo.py list

Here’s what I got when I tested it:

✓ Added: Buy groceries at SM Supermarket
✓ Added: Finish the quarterly report
✓ Added: Review pull request #42

📋 Your Todos:
--------------------------------------------------
○ 1. Buy groceries at SM Supermarket
○ 2. Finish the quarterly report
○ 3. Review pull request #42
--------------------------------------------------
Total: 3 | Completed: 0

✓ Completed: Finish the quarterly report

✓ Deleted todo #3

📋 Your Todos:
--------------------------------------------------
○ 1. Buy groceries at SM Supermarket
✓ 2. Finish the quarterly report
--------------------------------------------------
Total: 2 | Completed: 1

You can also peek at the data file:

cat todos.json
[
  {
    "id": 1,
    "task": "Buy groceries at SM Supermarket",
    "completed": false,
    "created_at": "2026-06-05 08:15:23"
  },
  {
    "id": 2,
    "task": "Finish the quarterly report",
    "completed": true,
    "created_at": "2026-06-05 08:15:24"
  }
]

Troubleshooting

“python: command not found” — Use python3 instead of python.

Strikethrough shows as weird characters on Windows — Use PowerShell or Windows Terminal instead of Command Prompt. Or comment out the ANSI escape line in list_todos().

Todos disappear between sessions — Make sure you’re running the script from the same directory. The todos.json file lives in your current working directory.

“Permission denied” on Linux/macOS — Run chmod +x todo.py to make it executable, then use ./todo.py instead.

Where to Go from Here

This is a working app, but it’s also a starting point. Some ideas to extend it:

The code is small enough to understand completely but flexible enough to grow with your skills.

The Bottom Line

Building your own tools is one of the most satisfying things you can do as a developer. You could use an existing todo app, sure. But now you have something that’s exactly what you need, runs anywhere Python runs, and you understand every single line.

That’s the difference between reading about programming and actually doing it. The only way to learn is to build things — so go add your first todo.


Filed under Tech & Gadgets
Last Update: June 5, 2026 by Felix AlterEgo
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Newest
Oldest Most Voted