Todo List Application
Abstract
A todo list is the canonical “first useful CLI app”. You learn file I/O, input validation, CRUD (Create / Read / Update / Delete), confirmations for destructive operations, and the basic shape of a long-running interactive program. In this project you will build the text-file version, fix several subtle bugs in the naïve approach, then evolve toward a richer todo manager with priorities, due dates, search, JSON storage, SQLite storage, and finally a Tkinter GUI.
You will leave comfortable with:
- Reading and writing text files safely.
- Persisting state between runs.
- Implementing CRUD operations.
- Designing menu-driven CLIs that do not rot as features pile up.
- Choosing a storage format (text, JSON, SQLite) for the right job.
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- Comfort with functions, lists, and basic file I/O.
Getting Started
Create the project
- Create folder
todo-apptodo-app. - Inside, create
todo.pytodo.py.
Write the code
Todo List Application
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-app> python todo.py
----------TO DO LIST----------
1. Add task
2. Delete task
3. Show tasks
4. Exit
Enter the choice: 1
Enter the task: Buy groceries
Task added successfully.C:\Users\username\Documents\todo-app> python todo.py
----------TO DO LIST----------
1. Add task
2. Delete task
3. Show tasks
4. Exit
Enter the choice: 1
Enter the task: Buy groceries
Task added successfully.Step-by-Step Explanation
1. 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.")Three things matter:
"a""a"mode appends instead of overwriting.with open(...)with open(...)guarantees the file is closed even on exception.encoding="utf-8"encoding="utf-8"makes the program work for non-English tasks.
2. View tasks
def view_tasks():
if not os.path.exists("todo.txt"):
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()]
if not tasks:
print("No tasks found.")
return
for i, t in enumerate(tasks, start=1):
print(f"{i}. {t}")def view_tasks():
if not os.path.exists("todo.txt"):
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()]
if not tasks:
print("No tasks found.")
return
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.- Empty lines are filtered out.
3. Delete a task
def delete_task():
tasks = read_tasks()
if not tasks:
print("Nothing to delete.")
return
show(tasks)
try:
n = int(input("Number to delete: "))
except ValueError:
print("Enter a number.")
return
if not 1 <= n <= len(tasks):
print("Out of range.")
return
removed = tasks.pop(n - 1)
write_tasks(tasks)
print(f"Deleted: {removed}")def delete_task():
tasks = read_tasks()
if not tasks:
print("Nothing to delete.")
return
show(tasks)
try:
n = int(input("Number to delete: "))
except ValueError:
print("Enter a number.")
return
if not 1 <= n <= len(tasks):
print("Out of range.")
return
removed = tasks.pop(n - 1)
write_tasks(tasks)
print(f"Deleted: {removed}")Reading the whole file, mutating in memory, and writing it back is fine for tens or hundreds of tasks. SQLite becomes preferable past that.
4. Confirm destructive operations
def delete_all():
if input("Are you sure? (y/n) ").lower() != "y":
print("Cancelled.")
return
open("todo.txt", "w").close()
print("All tasks deleted.")def delete_all():
if input("Are you sure? (y/n) ").lower() != "y":
print("Cancelled.")
return
open("todo.txt", "w").close()
print("All tasks deleted.")A y/ny/n confirmation prevents accidental wipes.
5. The main loop
def main():
while True:
print("\n1. Add 2. Delete 3. View 4. Clear all 5. Exit")
choice = input("> ").strip()
if choice == "1": add_task()
elif choice == "2": delete_task()
elif choice == "3": view_tasks()
elif choice == "4": delete_all()
elif choice == "5": break
else: print("Invalid choice.")def main():
while True:
print("\n1. Add 2. Delete 3. View 4. Clear all 5. Exit")
choice = input("> ").strip()
if choice == "1": add_task()
elif choice == "2": delete_task()
elif choice == "3": view_tasks()
elif choice == "4": delete_all()
elif choice == "5": break
else: print("Invalid choice.")Bugs in the Naïve Version
The most common errors in beginner todo apps:
- No
strip()strip()on read lines —"Buy bread\n""Buy bread\n"and"Buy bread""Buy bread"look identical but are not equal. open(...)open(...)withoutwithwith— leaks file descriptors on exception.- No empty-task check — pressing Enter creates blank tasks that confuse
deletedelete. - No off-by-one guard —
tasks[task_no]tasks[task_no]instead oftasks[task_no - 1]tasks[task_no - 1]deletes the wrong row. - No encoding — the program breaks the moment someone adds an emoji or accented character.
The version above fixes all five.
Add Priorities and Due Dates
Plain text loses structure the moment you want anything richer than a string. Move to JSON:
from pathlib import Path
import json
STORE = Path("todo.json")
def read_tasks():
return json.loads(STORE.read_text(encoding="utf-8")) if STORE.exists() else []
def write_tasks(tasks):
STORE.write_text(json.dumps(tasks, indent=2), encoding="utf-8")
def add(title, priority="medium", due=None):
tasks = read_tasks()
tasks.append({"title": title, "priority": priority,
"due": due, "done": False})
write_tasks(tasks)from pathlib import Path
import json
STORE = Path("todo.json")
def read_tasks():
return json.loads(STORE.read_text(encoding="utf-8")) if STORE.exists() else []
def write_tasks(tasks):
STORE.write_text(json.dumps(tasks, indent=2), encoding="utf-8")
def add(title, priority="medium", due=None):
tasks = read_tasks()
tasks.append({"title": title, "priority": priority,
"due": due, "done": False})
write_tasks(tasks)Each task is a dict with titletitle, prioritypriority, duedue (ISO date), and donedone. Now you can:
- Filter by priority.
- Sort by due date.
- Mark tasks done without deleting them.
- Show only open tasks.
Search and Filter
def search(keyword):
return [t for t in read_tasks() if keyword.lower() in t["title"].lower()]
def by_priority(priority):
return [t for t in read_tasks() if t["priority"] == priority and not t["done"]]
def overdue():
today = date.today().isoformat()
return [t for t in read_tasks() if t["due"] and t["due"] < today and not t["done"]]def search(keyword):
return [t for t in read_tasks() if keyword.lower() in t["title"].lower()]
def by_priority(priority):
return [t for t in read_tasks() if t["priority"] == priority and not t["done"]]
def overdue():
today = date.today().isoformat()
return [t for t in read_tasks() if t["due"] and t["due"] < today and not t["done"]]Migrate to SQLite
When you cross a thousand tasks, JSON’s “rewrite the whole file” model gets slow. SQLite handles thousands trivially:
import sqlite3, contextlib
DB = "todo.db"
with contextlib.closing(sqlite3.connect(DB)) as 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.commit()import sqlite3, contextlib
DB = "todo.db"
with contextlib.closing(sqlite3.connect(DB)) as 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.commit()Queries become SQL:
db.execute("INSERT INTO tasks(title, priority, due) VALUES(?, ?, ?)",
(title, priority, due))
db.execute("UPDATE tasks SET done = 1 WHERE id = ?", (task_id,))
rows = db.execute("SELECT * FROM tasks WHERE done = 0 ORDER BY due").fetchall()db.execute("INSERT INTO tasks(title, priority, due) VALUES(?, ?, ?)",
(title, priority, due))
db.execute("UPDATE tasks SET done = 1 WHERE id = ?", (task_id,))
rows = db.execute("SELECT * FROM tasks WHERE done = 0 ORDER BY due").fetchall()Now you have indexes, transactions, and concurrent-read support for free.
Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
| Crash on first run | File does not exist | if not Path("todo.txt").exists()if not Path("todo.txt").exists() |
| Tasks show with trailing newline | Did not strip | line.rstrip("\n")line.rstrip("\n") |
IndexErrorIndexError on delete | Off-by-one | tasks.pop(n - 1)tasks.pop(n - 1) and range check |
| Non-English text shows mojibake | Default encoding | Pass encoding="utf-8"encoding="utf-8" everywhere |
| File grows forever | Never truly cleared | open("todo.txt", "w").close()open("todo.txt", "w").close() truncates |
| Two instances corrupt the file | No locking | Use SQLite for multi-process |
Variations to Try
1. Mark as done (instead of delete)
Add a donedone flag. Show with strikethrough characters:
title = "".join(c + "̶" for c in task["title"])title = "".join(c + "̶" for c in task["title"])2. Edit a task
Prompt for a number, then a new title; rewrite in place.
3. Tags / categories
A tags: list[str]tags: list[str] field. Filter by tag with t for t in tasks if tag in t["tags"]t for t in tasks if tag in t["tags"].
4. Smart input
pip install dateparserpip install dateparser lets users type "finish report next friday""finish report next friday" and parse out a due date.
5. Recurring tasks
A recurrencerecurrence field (dailydaily, weeklyweekly). When marked done, schedule the next occurrence automatically.
6. Reminders
Combine with Simple Reminder App — when a task’s duedue arrives, fire a desktop notification.
7. Sync across devices
Save the JSON file to a synced folder (Dropbox/iCloud) or push to a small Flask backend (see Basic Web Server) so multiple devices see the same list.
8. CLI flags
With argparse or Typer:
todo add "Buy bread" --priority high --due 2025-06-01
todo list --priority high
todo done 3todo add "Buy bread" --priority high --due 2025-06-01
todo list --priority high
todo done 39. GUI version
A Tkinter window with a list of tasks, checkboxes for done, and an entry field. See Basic Music Player for the GUI pattern.
10. TUI version
richrich or textualtextual for a colorful in-terminal UI with keyboard navigation.
11. Export
“Export to CSV” / “Export to Markdown” — useful for sharing or backup.
12. Statistics
Show “completed this week”, “average completion time”, “most common priority”.
Best Practices Demonstrated
- Confirm destructive operations before performing them.
- Validate input at the boundary (numeric, in-range, non-empty).
- Always use
with open(...)with open(...)for file I/O. - Pass
encoding="utf-8"encoding="utf-8"explicitly on every text file. - Move from text → JSON → SQLite as needs grow; do not skip ahead.
Real-World Applications
- Personal task management.
- Team shared todo (with the Flask variant).
- Bug-tracker prototypes for small projects.
- Shopping list / grocery apps.
- Build-step tracker for repetitive workflows.
Educational Value
- CRUD operations — the foundation of every data app.
- File I/O — text vs. JSON vs. SQLite trade-offs.
- Input validation — handling the messy reality of humans typing.
- Confirmation flows — UX for irreversible actions.
- Refactoring — going from a 50-line script to a tested, indexed, multi-user app.
Next Steps
- Add mark as done (without deletion).
- Switch storage to JSON, then SQLite.
- Add priorities, due dates, tags.
- Wrap with a Tkinter GUI or a Flask web app.
- Layer reminders (cross-link with Simple Reminder App).
- See Todo List GUI for the next step.
Conclusion
You built a complete todo app and learned the path from a text file to a proper relational schema. Every project you write from here will reuse these CRUD, persistence, and confirmation patterns. 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
