Skip to content

Todo List Application (CLI)

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.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 open and UTF-8 encoding.
  • The full CRUD cycle on a flat text file.
  • Why confirmation flows matter for destructive operations.
  • The architectural path from todo.txt → JSON → SQLite.
  • How to grow a 30-line script into a tool you would actually use.
  • Python 3.6 or above.
  • A text editor or IDE.
  • Comfort with input, if/elif/else, and while.
  • Familiarity with the lighter Todo (basics) walkthrough.
  1. Create folder todo-list.
  2. Inside, create todo.py.
  3. The script will create todo.txt on first save.
Todo List App pch.viewSource
Todo List App
# 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> 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.

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
import os, sys, time
  • os for the restart trick.
  • sys for sys.exit().
  • time for the sleep before restart.
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.")
  • "a" opens for append — does not erase existing content.
  • .strip() removes trailing whitespace including the newline from Enter.
  • Empty task guard avoids accidental blank entries.
todo.py
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) gives 1-based numbering for the user.
  • The filter if line.strip() skips blank lines that may sneak in.
todo.py
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.

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

todo.py
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() is the clean way to quit.
  • os.execv replaces the current process with a fresh interpreter — true restart with no state.
todo.py
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() makes e, E, e , E all equivalent.

Architecture: From txt → JSON → SQLite

Section titled “Architecture: From txt → JSON → SQLite”

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

ProblemCauseFix
FileNotFoundError on first runFile created lazilyGuard with os.path.exists
Tasks shown with trailing newlineForgot to stripline.rstrip("\n")
IndexError on deleteOff-by-onetasks.pop(n - 1) with range check
Mojibake on non-English tasksDefault encodingAlways encoding="utf-8"
Empty task added on Enter-onlyNo empty checkif not task: return
Two terminals corrupt the fileNo lockingMove to SQLite for concurrent access
Restart leaves stale stateManual reset of globals incompleteUse os.execv for clean restart

Add a done flag. Show [x] or [ ] prefix. Filter with view --pending.

high, medium, low — sort by priority on view.

due: YYYY-MM-DD. Highlight overdue in red on output.

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

A tags: list[str] field. Filter with view --tag work.

view --search keyword.

“Edit task #3” → prompt for new title.

recurrence: daily | weekly | monthly. On complete, schedule the next occurrence.

Combine with Simple Reminder App — fire a desktop notification when due arrives.

cli
todo add "Buy bread" --priority high --due 2026-06-01
todo list --pending --tag groceries
todo done 3
todo delete 5

Tkinter window with a Listbox of tasks, checkboxes for done, and an entry field for new tasks.

Flask app (see Basic Web Server) with HTMX or React, JSON API in the middle.

Write the JSON to a shared cloud folder (Dropbox / iCloud) or push to a server. Eventual-consistency conflict resolution.

“Start working on task #3” → 25-minute timer fires, breaks logged.

Tasks completed this week / month. Streaks, longest-pending task, average time-to-done.

todo export markdown > tasks.md → renders nicely on GitHub.

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.

  • Always with open(...) for file I/O.
  • 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.
  • Personal task management.
  • Project to-do lists.
  • Quick reminders before they become reminders.
  • Shopping / grocery lists.
  • Bug tracker prototypes for solo projects.
  • CRUD operations on a flat text file.
  • Defensive UX — confirmations, validations, friendly errors.
  • Storage trade-offs — text vs. JSON vs. SQLite.
  • Process controlsys.exit, os.execv, time.sleep.
  • 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.

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

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading