Skip to content

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

  1. Create folder todo-apptodo-app.
  2. Inside, create todo.pytodo.py.

Write the code

Todo List ApplicationSource
Todo List Application
# 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 List Application
# 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

command
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.
command
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

todo.py
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.")
todo.py
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

todo.py
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}")
todo.py
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

todo.py
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}")
todo.py
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

todo.py
def delete_all():
    if input("Are you sure? (y/n) ").lower() != "y":
        print("Cancelled.")
        return
    open("todo.txt", "w").close()
    print("All tasks deleted.")
todo.py
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

todo.py
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.")
todo.py
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:

  1. No strip()strip() on read lines"Buy bread\n""Buy bread\n" and "Buy bread""Buy bread" look identical but are not equal.
  2. open(...)open(...) without withwith — leaks file descriptors on exception.
  3. No empty-task check — pressing Enter creates blank tasks that confuse deletedelete.
  4. No off-by-one guardtasks[task_no]tasks[task_no] instead of tasks[task_no - 1]tasks[task_no - 1] deletes the wrong row.
  5. 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:

json_store.py
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)
json_store.py
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

filter.py
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"]]
filter.py
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:

sqlite_store.py
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()
sqlite_store.py
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:

queries.py
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()
queries.py
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

ProblemCauseFix
Crash on first runFile does not existif not Path("todo.txt").exists()if not Path("todo.txt").exists()
Tasks show with trailing newlineDid not stripline.rstrip("\n")line.rstrip("\n")
IndexErrorIndexError on deleteOff-by-onetasks.pop(n - 1)tasks.pop(n - 1) and range check
Non-English text shows mojibakeDefault encodingPass encoding="utf-8"encoding="utf-8" everywhere
File grows foreverNever truly clearedopen("todo.txt", "w").close()open("todo.txt", "w").close() truncates
Two instances corrupt the fileNo lockingUse SQLite for multi-process

Variations to Try

1. Mark as done (instead of delete)

Add a donedone flag. Show with strikethrough characters:

strike.py
title = "".join(c + "̶" for c in task["title"])
strike.py
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:

cli
todo add "Buy bread" --priority high --due 2025-06-01
todo list --priority high
todo done 3
cli
todo add "Buy bread" --priority high --due 2025-06-01
todo list --priority high
todo done 3

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

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 coffee

Was this page helpful?

Let us know how we did