Skip to content

Todo List Application

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.
  • Python 3.6 or above.
  • A text editor or IDE.
  • Comfort with functions, lists, and basic file I/O.
  1. Create folder todo-app.
  2. Inside, create todo.py.
Todo List Application pch.viewSource
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.")
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.

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.

diagram Diagram mermaid
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" 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.
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) gives 1-based numbering for the user.
  • Empty lines are filtered out.
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.

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/n confirmation prevents accidental wipes.

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

The most common errors in beginner todo apps:

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

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)

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.
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"]]

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()

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()

Now you have indexes, transactions, and concurrent-read support for free.

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

Add a done flag. Show with strikethrough characters:

strike.py
title = "".join(c + "̶" for c in task["title"])

Prompt for a number, then a new title; rewrite in place.

A tags: list[str] field. Filter by tag with t for t in tasks if tag in t["tags"].

pip install dateparser lets users type "finish report next friday" and parse out a due date.

A recurrence field (daily, weekly). When marked done, schedule the next occurrence automatically.

Combine with Simple Reminder App — when a task’s due arrives, fire a desktop notification.

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.

With argparse or Typer:

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

A Tkinter window with a list of tasks, checkboxes for done, and an entry field. See Basic Music Player for the GUI pattern.

rich or textual for a colorful in-terminal UI with keyboard navigation.

“Export to CSV” / “Export to Markdown” — useful for sharing or backup.

Show “completed this week”, “average completion time”, “most common priority”.

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

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

pch.feedbackHeading

pch.feedbackSubheading