Skip to content

Todo List Application (CLI)

Abstract

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.txttodo.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 openwith 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.txttodo.txt → JSON → SQLite.
  • How to grow a 30-line script into a tool you would actually use.

Prerequisites

  • Python 3.6 or above.
  • A text editor or IDE.
  • Comfort with inputinput, if/elif/elseif/elif/else, and whilewhile.
  • Familiarity with the lighter Todo (basics) walkthrough.

Getting Started

Create the project

  1. Create folder todo-listtodo-list.
  2. Inside, create todo.pytodo.py.
  3. The script will create todo.txttodo.txt on first save.

Write the code

Todo List AppSource
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.")
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.")

Run it

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

Step-by-Step Explanation

1. Imports

todo.py
import os, sys, time
todo.py
import os, sys, time
  • osos for the restart trick.
  • syssys for sys.exit()sys.exit().
  • timetime for the sleep before restart.

2. 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.")
  • "a""a" opens for append — does not erase existing content.
  • .strip().strip() removes trailing whitespace including the newline from Enter.
  • Empty task guard avoids accidental blank entries.

3. View tasks

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}")
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)enumerate(..., start=1) gives 1-based numbering for the user.
  • The filter if line.strip()if line.strip() skips blank lines that may sneak in.

4. Delete by number

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

5. Delete all (with confirmation)

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

6. Exit, Restart, Help

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")
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()sys.exit() is the clean way to quit.
  • os.execvos.execv replaces the current process with a fresh interpreter — true restart with no state.

7. The main loop

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.")
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().strip().upper() makes ee, EE, e e , EE all equivalent.

Architecture: From txt → JSON → SQLite

A todo.txttodo.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.

Step 1: JSON

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

Step 2: SQLite

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

Common Mistakes

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

Variations to Try

1. Mark done (instead of delete)

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

2. Priorities

highhigh, mediummedium, lowlow — sort by priority on view.

3. Due dates

due: YYYY-MM-DDdue: 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 ''}")
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 ''}")

4. Tags

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

view --search keywordview --search keyword.

6. Edit

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

7. Recurring tasks

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

8. Reminders

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

9. CLI flags with argparseargparse or typertyper

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

10. GUI version

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

11. Web frontend

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

12. Sync across devices

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

13. Pomodoro integration

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

14. Statistics

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

15. Export

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

Companion Tutorial

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.

Best Practices Demonstrated

  • Always with open(...)with open(...) for file I/O.
  • encoding="utf-8"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.

Real-World Applications

  • Personal task management.
  • Project to-do lists.
  • Quick reminders before they become reminders.
  • Shopping / grocery lists.
  • Bug tracker prototypes for solo projects.

Educational Value

  • CRUD operations on a flat text file.
  • Defensive UX — confirmations, validations, friendly errors.
  • Storage trade-offs — text vs. JSON vs. SQLite.
  • Process controlsys.exitsys.exit, os.execvos.execv, time.sleeptime.sleep.

Next Steps

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

Conclusion

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

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did