Todo List Application
Abstract
Section titled “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
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- Comfort with functions, lists, and basic file I/O.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create folder
todo-app. - Inside, create
todo.py.
Write the code
Section titled “Write the code”Todo List Application
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-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.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. Add a task
Section titled “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.")Three things matter:
"a"mode appends instead of overwriting.with open(...)guarantees the file is closed even on exception.encoding="utf-8"makes the program work for non-English tasks.
2. View tasks
Section titled “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}")enumerate(..., start=1)gives 1-based numbering for the user.- Empty lines are filtered out.
3. Delete a task
Section titled “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}")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
Section titled “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.")A y/n confirmation prevents accidental wipes.
5. The main loop
Section titled “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.")Bugs in the Naïve Version
Section titled “Bugs in the Naïve Version”The most common errors in beginner todo apps:
- No
strip()on read lines —"Buy bread\n"and"Buy bread"look identical but are not equal. open(...)withoutwith— leaks file descriptors on exception.- No empty-task check — pressing Enter creates blank tasks that confuse
delete. - No off-by-one guard —
tasks[task_no]instead oftasks[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
Section titled “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)Each task is a dict with title, priority, due (ISO date), and done. Now you can:
- Filter by priority.
- Sort by due date.
- Mark tasks done without deleting them.
- Show only open tasks.
Search and Filter
Section titled “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"]]Migrate to SQLite
Section titled “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()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()Now you have indexes, transactions, and concurrent-read support for free.
Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| Crash on first run | File does not exist | if not Path("todo.txt").exists() |
| Tasks show with trailing newline | Did not strip | line.rstrip("\n") |
IndexError on delete | Off-by-one | tasks.pop(n - 1) and range check |
| Non-English text shows mojibake | Default encoding | Pass encoding="utf-8" everywhere |
| File grows forever | Never truly cleared | open("todo.txt", "w").close() truncates |
| Two instances corrupt the file | No locking | Use SQLite for multi-process |
Variations to Try
Section titled “Variations to Try”1. Mark as done (instead of delete)
Section titled “1. Mark as done (instead of delete)”Add a done flag. Show with strikethrough characters:
title = "".join(c + "̶" for c in task["title"])2. Edit a task
Section titled “2. Edit a task”Prompt for a number, then a new title; rewrite in place.
3. Tags / categories
Section titled “3. Tags / categories”A tags: list[str] field. Filter by tag with t for t in tasks if tag in t["tags"].
4. Smart input
Section titled “4. Smart input”pip install dateparser lets users type "finish report next friday" and parse out a due date.
5. Recurring tasks
Section titled “5. Recurring tasks”A recurrence field (daily, weekly). When marked done, schedule the next occurrence automatically.
6. Reminders
Section titled “6. Reminders”Combine with Simple Reminder App — when a task’s due arrives, fire a desktop notification.
7. Sync across devices
Section titled “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
Section titled “8. CLI flags”With argparse or Typer:
todo add "Buy bread" --priority high --due 2025-06-01
todo list --priority high
todo done 39. GUI version
Section titled “9. 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
Section titled “10. TUI version”rich or textual for a colorful in-terminal UI with keyboard navigation.
11. Export
Section titled “11. Export”“Export to CSV” / “Export to Markdown” — useful for sharing or backup.
12. Statistics
Section titled “12. Statistics”Show “completed this week”, “average completion time”, “most common priority”.
Best Practices Demonstrated
Section titled “Best Practices Demonstrated”- Confirm destructive operations before performing them.
- Validate input at the boundary (numeric, in-range, non-empty).
- Always use
with open(...)for file I/O. - Pass
encoding="utf-8"explicitly on every text file. - Move from text → JSON → SQLite as needs grow; do not skip ahead.
Real-World Applications
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading