Todo List Application (CLI)
Abstract
Section titled “Abstract”The Todo List Application is the canonical first useful program in any language. It exercises file I/O, persistence, CRUD operations, input validation, menu design, and confirmation flows in under 150 lines. In this expanded tutorial you build the polished command-line version with Add / View / Delete / Delete-all / Exit / Restart / Help commands stored in a plain todo.txt, then evolve toward a real productivity tool: priorities, due dates, completion marks, JSON storage, SQLite, and a Tkinter GUI.
You will learn:
- Robust file I/O with
with openand UTF-8 encoding. - The full CRUD cycle on a flat text file.
- Why confirmation flows matter for destructive operations.
- The architectural path from
todo.txt→ JSON → SQLite. - How to grow a 30-line script into a tool you would actually use.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- Comfort with
input,if/elif/else, andwhile. - Familiarity with the lighter Todo (basics) walkthrough.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create folder
todo-list. - Inside, create
todo.py. - The script will create
todo.txton first save.
Write the code
Section titled “Write the code”Todo List App
pch.viewSource# Todo Application
# Importing Modules
import os
import time
import sys
import datetime
# Defining Functions
def add_task():
print("Add Task")
print("---------")
task = input("Enter Task: ")
with open("todo.txt", "a") as f:
f.write(task + "\n")
print("Task Added Successfully.")
def view_task():
print("View Task")
print("---------")
with open("todo.txt", "r") as f:
tasks = f.readlines()
if len(tasks) == 0:
print("No Tasks Found.")
else:
for i in range(len(tasks)):
print(str(i + 1) + ". " + tasks[i].strip("\n"))
def delete_task():
print("Delete Task")
print("------------")
with open("todo.txt", "r") as f:
tasks = f.readlines()
if len(tasks) == 0:
print("No Tasks Found.")
else:
for i in range(len(tasks)):
print(str(i + 1) + ". " + tasks[i].strip("\n"))
task_no = int(input("Enter Task Number to Delete: "))
if task_no > len(tasks):
print("Invalid Task Number.")
else:
del tasks[task_no - 1]
with open("todo.txt", "w") as f:
for task in tasks:
f.write(task)
print("Task Deleted Successfully.")
def delete_all_task():
print("Delete All Task")
print("----------------")
with open("todo.txt", "r") as f:
tasks = f.readlines()
if len(tasks) == 0:
print("No Tasks Found.")
else:
for i in range(len(tasks)):
print(str(i + 1) + ". " + tasks[i].strip("\n"))
confirm = input("Are you sure you want to delete all tasks? (Y/N): ")
if confirm in ("Y", "y"):
with open("todo.txt", "w") as f:
f.write("")
print("All Tasks Deleted Successfully.")
elif confirm in ("N", "n"):
print("No Tasks Deleted.")
else:
print("Invalid Choice.")
def exit():
print("Exit")
print("-----")
confirm = input("Are you sure you want to exit? (Y/N): ")
if confirm in ("Y", "y"):
print("Exiting...")
time.sleep(1)
sys.exit()
elif confirm in ("N", "n"):
print("Not Exiting.")
else:
print("Invalid Choice.")
def restart():
print("Restart")
print("--------")
confirm = input("Are you sure you want to restart? (Y/N): ")
if confirm in ("Y", "y"):
print("Restarting...")
time.sleep(1)
os.system("python todo.py")
elif confirm in ("N", "n"):
print("Not Restarting.")
else:
print("Invalid Choice.")
def help():
print("Help")
print("----")
print("Add Task: Add a task to the todo list.")
print("View Task: View all tasks in the todo list.")
print("Delete Task: Delete a task from the todo list.")
print("Delete All Task: Delete all tasks from the todo list.")
print("Exit: Exit the application.")
print("Restart: Restart the application.")
print("Help: View help.")
# Main Program
print("Todo Application")
print("----------------")
print("Select Operation.")
print("1. Add Task")
print("2. View Task")
print("3. Delete Task")
print("4. Delete All Task")
print("E. Exit")
print("R. Restart")
print("H. Help")
while True:
choice = input("Enter Choice (1/2/3/4/E/R/H): ")
if choice == "1":
add_task()
elif choice == "2":
view_task()
elif choice == "3":
delete_task()
elif choice == "4":
delete_all_task()
elif choice.upper() == "E":
exit()
elif choice.upper() == "R":
restart()
elif choice.upper() == "H":
help()
else:
print("Invalid Choice.") Run it
Section titled “Run it”C:\Users\username\Documents\todo> python todo.py
Todo Application
1. Add task 2. View task 3. Delete task 4. Delete all
E. Exit R. Restart H. Help
Enter Choice: 1
Enter Task: Going for the Google summit
Task Added Successfully.How it fits together
Section titled “How it fits together”Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.
flowchart TD
RUN(["python todo.py"])
add_task("add_task")
view_task("view_task")
delete_task("delete_task")
delete_all_task("delete_all_task")
exit("exit")
restart("restart")
help("help")
RUN --> add_task
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. Imports
Section titled “1. Imports”import os, sys, timeosfor the restart trick.sysforsys.exit().timefor the sleep before restart.
2. Add a task
Section titled “2. Add a task”def add_task():
task = input("Enter task: ").strip()
if not task:
print("Empty task ignored.")
return
with open("todo.txt", "a", encoding="utf-8") as f:
f.write(task + "\n")
print("Task added.")"a"opens for append — does not erase existing content..strip()removes trailing whitespace including the newline from Enter.- Empty task guard avoids accidental blank entries.
3. View tasks
Section titled “3. View tasks”def view_tasks():
if not os.path.exists("todo.txt") or os.path.getsize("todo.txt") == 0:
print("No tasks yet.")
return
with open("todo.txt", encoding="utf-8") as f:
tasks = [line.rstrip("\n") for line in f if line.strip()]
for i, t in enumerate(tasks, start=1):
print(f"{i}. {t}")enumerate(..., start=1)gives 1-based numbering for the user.- The filter
if line.strip()skips blank lines that may sneak in.
4. Delete by number
Section titled “4. Delete by number”def delete_task():
if not os.path.exists("todo.txt"):
print("Nothing to delete."); return
with open("todo.txt", encoding="utf-8") as f:
tasks = [line.rstrip("\n") for line in f if line.strip()]
if not tasks:
print("Nothing to delete."); return
for i, t in enumerate(tasks, 1): print(f"{i}. {t}")
try:
n = int(input("Number to delete: "))
except ValueError:
print("Enter a whole number."); return
if not 1 <= n <= len(tasks):
print("Out of range."); return
removed = tasks.pop(n - 1)
with open("todo.txt", "w", encoding="utf-8") as f:
f.writelines(t + "\n" for t in tasks)
print(f"Deleted: {removed}")The “load all, mutate, write all back” pattern is fine for small files (thousands of lines). Beyond that, SQLite is the right answer.
5. Delete all (with confirmation)
Section titled “5. Delete all (with confirmation)”def delete_all():
if input("Delete EVERY task? (Y/N): ").strip().lower() != "y":
print("Cancelled."); return
open("todo.txt", "w").close()
print("All tasks deleted.")Confirmation flows are not optional — accidental “delete all” is the #1 way users lose data.
6. Exit, Restart, Help
Section titled “6. Exit, Restart, Help”def exit_app():
if input("Exit? (Y/N): ").strip().lower() == "y":
print("Goodbye.")
sys.exit()
def restart():
if input("Restart? (Y/N): ").strip().lower() == "y":
print("Restarting..."); time.sleep(1)
os.execv(sys.executable, [sys.executable] + sys.argv)
def show_help():
print("Add / View / Delete / Delete All / Exit / Restart / Help")sys.exit()is the clean way to quit.os.execvreplaces the current process with a fresh interpreter — true restart with no state.
7. The main loop
Section titled “7. The main loop”while True:
choice = input("Choice (1/2/3/4/E/R/H): ").strip().upper()
if choice == "1": add_task()
elif choice == "2": view_tasks()
elif choice == "3": delete_task()
elif choice == "4": delete_all()
elif choice == "E": exit_app()
elif choice == "R": restart()
elif choice == "H": show_help()
else: print("Invalid choice.").strip().upper() makes e, E, e , E all equivalent.
Architecture: From txt → JSON → SQLite
Section titled “Architecture: From txt → JSON → SQLite”A todo.txt is great for ≤ ~500 tasks. Pain points beyond that:
- No structured fields — priority, due date, tags, status.
- Editing a single task means rewriting the whole file.
- Concurrent access from two terminals corrupts it.
Step 1: JSON
Section titled “Step 1: JSON”import json
from pathlib import Path
STORE = Path("todo.json")
def read():
return json.loads(STORE.read_text(encoding="utf-8")) if STORE.exists() else []
def write(tasks):
STORE.write_text(json.dumps(tasks, indent=2), encoding="utf-8")
def add(title, priority="medium", due=None):
tasks = read()
tasks.append({
"id": len(tasks) + 1,
"title": title, "priority": priority, "due": due, "done": False,
})
write(tasks)Step 2: SQLite
Section titled “Step 2: SQLite”import sqlite3
db = sqlite3.connect("todo.db")
db.execute("""CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
priority TEXT DEFAULT 'medium',
due TEXT,
done INTEGER DEFAULT 0,
created TEXT DEFAULT CURRENT_TIMESTAMP
)""")
db.execute("INSERT INTO tasks(title, priority) VALUES(?, ?)", ("Buy bread", "high"))
db.commit()
# query
for row in db.execute("SELECT id, title FROM tasks WHERE done = 0 ORDER BY due"):
print(row)Now you have indexes, transactions, concurrent reads, and SQL queries — all from one Python module.
Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
FileNotFoundError on first run | File created lazily | Guard with os.path.exists |
| Tasks shown with trailing newline | Forgot to strip | line.rstrip("\n") |
IndexError on delete | Off-by-one | tasks.pop(n - 1) with range check |
| Mojibake on non-English tasks | Default encoding | Always encoding="utf-8" |
| Empty task added on Enter-only | No empty check | if not task: return |
| Two terminals corrupt the file | No locking | Move to SQLite for concurrent access |
| Restart leaves stale state | Manual reset of globals incomplete | Use os.execv for clean restart |
Variations to Try
Section titled “Variations to Try”1. Mark done (instead of delete)
Section titled “1. Mark done (instead of delete)”Add a done flag. Show [x] or [ ] prefix. Filter with view --pending.
2. Priorities
Section titled “2. Priorities”high, medium, low — sort by priority on view.
3. Due dates
Section titled “3. Due dates”due: YYYY-MM-DD. Highlight overdue in red on output.
from datetime import date
RED, RESET = "\033[91m", "\033[0m"
overdue = task["due"] and task["due"] < date.today().isoformat()
print(f"{RED if overdue else ''}{task['title']}{RESET if overdue else ''}")4. Tags
Section titled “4. Tags”A tags: list[str] field. Filter with view --tag work.
5. Search
Section titled “5. Search”view --search keyword.
6. Edit
Section titled “6. Edit”“Edit task #3” → prompt for new title.
7. Recurring tasks
Section titled “7. Recurring tasks”recurrence: daily | weekly | monthly. On complete, schedule the next occurrence.
8. Reminders
Section titled “8. Reminders”Combine with Simple Reminder App — fire a desktop notification when due arrives.
9. CLI flags with argparse or typer
Section titled “9. CLI flags with argparse or typer”todo add "Buy bread" --priority high --due 2026-06-01
todo list --pending --tag groceries
todo done 3
todo delete 510. GUI version
Section titled “10. GUI version”Tkinter window with a Listbox of tasks, checkboxes for done, and an entry field for new tasks.
11. Web frontend
Section titled “11. Web frontend”Flask app (see Basic Web Server) with HTMX or React, JSON API in the middle.
12. Sync across devices
Section titled “12. Sync across devices”Write the JSON to a shared cloud folder (Dropbox / iCloud) or push to a server. Eventual-consistency conflict resolution.
13. Pomodoro integration
Section titled “13. Pomodoro integration”“Start working on task #3” → 25-minute timer fires, breaks logged.
14. Statistics
Section titled “14. Statistics”Tasks completed this week / month. Streaks, longest-pending task, average time-to-done.
15. Export
Section titled “15. Export”todo export markdown > tasks.md → renders nicely on GitHub.
Companion Tutorial
Section titled “Companion Tutorial”This page covers the more detailed CLI walkthrough. For the introduction with a simpler scope, see Todo (basics) — same script, simpler narrative. For the GUI version, see the upcoming “Todo Tkinter” project.
Best Practices Demonstrated
Section titled “Best Practices Demonstrated”- Always
with open(...)for file I/O. encoding="utf-8"always to avoid silent corruption.- Confirm destructive operations.
- Validate input at the moment it enters.
- Migrate storage as you grow — txt → JSON → SQLite, never the reverse.
Real-World Applications
Section titled “Real-World Applications”- Personal task management.
- Project to-do lists.
- Quick reminders before they become reminders.
- Shopping / grocery lists.
- Bug tracker prototypes for solo projects.
Educational Value
Section titled “Educational Value”- CRUD operations on a flat text file.
- Defensive UX — confirmations, validations, friendly errors.
- Storage trade-offs — text vs. JSON vs. SQLite.
- Process control —
sys.exit,os.execv,time.sleep.
Next Steps
Section titled “Next Steps”- Add mark-as-done (don’t delete).
- Migrate to JSON storage.
- Add priorities and due dates.
- Wrap with a Tkinter GUI.
- Layer reminders via Simple Reminder App.
- Cross-link with Todo (basics) for the simpler intro.
Conclusion
Section titled “Conclusion”You built a complete CLI todo manager with persistence, CRUD, and confirmation flows — the same architecture used by every text-file-backed productivity tool from taskwarrior to early WordPress drafts. Every step beyond (priorities, due dates, GUIs, sync) reuses these primitives. Full source on GitHub. Explore more productivity projects on Python Central Hub.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading