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 fileos— check if the file existsdatetime— timestamp each todoTODO_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:
- Add due dates and sort by deadline
- Add priority levels (high, medium, low)
- Export to CSV or Markdown for sharing
- Build a web interface with Flask or FastAPI — if you’re interested in how modern tools are transforming app development, this is a great way to learn
- Containerize it with Docker — check out my guide on Docker Compose for local development if you want to package this properly
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.