Todo List Application (CLI)
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.txttodo.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 openwith 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.txttodo.txt→ JSON → SQLite. - How to grow a 30-line script into a tool you would actually use.
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- Comfort with
inputinput,if/elif/elseif/elif/else, andwhilewhile. - Familiarity with the lighter Todo (basics) walkthrough.
Getting Started
Create the project
- Create folder
todo-listtodo-list. - Inside, create
todo.pytodo.py. - The script will create
todo.txttodo.txton first save.
Write the code
Todo List App
Source# 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.")# 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
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.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.Step-by-Step Explanation
1. Imports
import os, sys, timeimport os, sys, timeososfor the restart trick.syssysforsys.exit()sys.exit().timetimefor the sleep before restart.
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.")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""a"opens for append — does not erase existing content..strip().strip()removes trailing whitespace including the newline from Enter.- Empty task guard avoids accidental blank entries.
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}")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)enumerate(..., start=1)gives 1-based numbering for the user.- The filter
if line.strip()if line.strip()skips blank lines that may sneak in.
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}")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)
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.")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
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")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()sys.exit()is the clean way to quit.os.execvos.execvreplaces the current process with a fresh interpreter — true restart with no state.
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.")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().strip().upper() makes ee, EE, e e , EE all equivalent.
Architecture: From txt → JSON → SQLite
A todo.txttodo.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
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)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
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)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
| Problem | Cause | Fix |
|---|---|---|
FileNotFoundErrorFileNotFoundError on first run | File created lazily | Guard with os.path.existsos.path.exists |
| Tasks shown with trailing newline | Forgot to strip | line.rstrip("\n")line.rstrip("\n") |
IndexErrorIndexError on delete | Off-by-one | tasks.pop(n - 1)tasks.pop(n - 1) with range check |
| Mojibake on non-English tasks | Default encoding | Always encoding="utf-8"encoding="utf-8" |
| Empty task added on Enter-only | No empty check | if not task: returnif 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.execvos.execv for clean restart |
Variations to Try
1. Mark done (instead of delete)
Add a donedone flag. Show [x][x] or [ ][ ] prefix. Filter with view --pendingview --pending.
2. Priorities
highhigh, mediummedium, lowlow — sort by priority on view.
3. Due dates
due: YYYY-MM-DDdue: 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 ''}")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
A tags: list[str]tags: list[str] field. Filter with view --tag workview --tag work.
5. Search
view --search keywordview --search keyword.
6. Edit
“Edit task #3” → prompt for new title.
7. Recurring tasks
recurrence: daily | weekly | monthlyrecurrence: daily | weekly | monthly. On complete, schedule the next occurrence.
8. Reminders
Combine with Simple Reminder App — fire a desktop notification when duedue arrives.
9. CLI flags with argparseargparse or typertyper
todo add "Buy bread" --priority high --due 2026-06-01
todo list --pending --tag groceries
todo done 3
todo delete 5todo add "Buy bread" --priority high --due 2026-06-01
todo list --pending --tag groceries
todo done 3
todo delete 510. GUI version
Tkinter window with a ListboxListbox of tasks, checkboxes for done, and an entry field for new tasks.
11. Web frontend
Flask app (see Basic Web Server) with HTMX or React, JSON API in the middle.
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
“Start working on task #3” → 25-minute timer fires, breaks logged.
14. Statistics
Tasks completed this week / month. Streaks, longest-pending task, average time-to-done.
15. Export
todo export markdown > tasks.mdtodo export markdown > tasks.md → renders nicely on GitHub.
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
- Always
with open(...)with open(...)for file I/O. encoding="utf-8"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
- Personal task management.
- Project to-do lists.
- Quick reminders before they become reminders.
- Shopping / grocery lists.
- Bug tracker prototypes for solo projects.
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.exitsys.exit,os.execvos.execv,time.sleeptime.sleep.
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
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 taskwarriortaskwarrior 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.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
