Skip to content
Happy Programming Guide
Start learning
Programming Projects

Python Project: A To-Do List That Saves to a File

Build a command-line to-do app in Python that remembers your tasks between runs. Covers lists, dictionaries, JSON files and a clean menu loop.

This project adds the thing most beginner programs are missing: data that survives closing the program. You will store tasks in a JSON file and load them back on the next run.

What you will build#

Output
TO-DO  (2 of 3 done)
  1. [x] Buy milk
  2. [ ] Finish Python guide
  3. [x] Call the bank

a) add   d) done   r) remove   q) quit
Choice:

Step 1: decide the shape of your data#

Before writing any code, decide what one task looks like:

Python
{"text": "Buy milk", "done": False}

And the whole list is simply a list of those:

Python
tasks = [
    {"text": "Buy milk", "done": True},
    {"text": "Finish Python guide", "done": False},
]

Getting this shape right first makes everything after it easier. See Python dictionaries explained.

Step 2: saving and loading#

Python
import json

FILE = "tasks.json"

def load_tasks():
    try:
        with open(FILE) as f:
            return json.load(f)
    except FileNotFoundError:
        return []            # first run — start empty
    except json.JSONDecodeError:
        print("tasks.json is damaged. Starting fresh.")
        return []

def save_tasks(tasks):
    with open(FILE, "w") as f:
        json.dump(tasks, f, indent=2)

Two failure cases are handled: the file not existing yet, and the file being corrupted. Both would otherwise crash on startup.

Step 3: the operations#

Python
def add_task(tasks, text):
    text = text.strip()
    if not text:
        return "Task cannot be empty."
    tasks.append({"text": text, "done": False})
    return f"Added: {text}"

def toggle_task(tasks, number):
    if number < 1 or number > len(tasks):
        return "No task with that number."
    task = tasks[number - 1]
    task["done"] = not task["done"]
    return ("Done: " if task["done"] else "Reopened: ") + task["text"]

def remove_task(tasks, number):
    if number < 1 or number > len(tasks):
        return "No task with that number."
    removed = tasks.pop(number - 1)
    return f"Removed: {removed['text']}"

Each returns a message instead of printing. That keeps them testable and means all output happens in one place.

Note number - 1: people count from 1, lists count from 0. Converting once at the boundary is much safer than remembering everywhere.

Step 4: showing the list#

Python
def show_tasks(tasks):
    if not tasks:
        print("
Nothing to do. Add something with 'a'.")
        return

    done = sum(1 for t in tasks if t["done"])
    print(f"
TO-DO  ({done} of {len(tasks)} done)")

    for i, task in enumerate(tasks, start=1):
        mark = "x" if task["done"] else " "
        print(f"  {i}. [{mark}] {task['text']}")

The finished program#

Python
import json

FILE = "tasks.json"

def load_tasks():
    try:
        with open(FILE) as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return []

def save_tasks(tasks):
    with open(FILE, "w") as f:
        json.dump(tasks, f, indent=2)

def add_task(tasks, text):
    text = text.strip()
    if not text:
        return "Task cannot be empty."
    tasks.append({"text": text, "done": False})
    return f"Added: {text}"

def toggle_task(tasks, number):
    if number < 1 or number > len(tasks):
        return "No task with that number."
    task = tasks[number - 1]
    task["done"] = not task["done"]
    return ("Done: " if task["done"] else "Reopened: ") + task["text"]

def remove_task(tasks, number):
    if number < 1 or number > len(tasks):
        return "No task with that number."
    return f"Removed: {tasks.pop(number - 1)['text']}"

def show_tasks(tasks):
    if not tasks:
        print("
Nothing to do. Add something with 'a'.")
        return
    done = sum(1 for t in tasks if t["done"])
    print(f"
TO-DO  ({done} of {len(tasks)} done)")
    for i, task in enumerate(tasks, start=1):
        mark = "x" if task["done"] else " "
        print(f"  {i}. [{mark}] {task['text']}")

def ask_number(prompt):
    try:
        return int(input(prompt))
    except ValueError:
        return -1

def main():
    tasks = load_tasks()

    while True:
        show_tasks(tasks)
        choice = input("
a) add   d) done   r) remove   q) quit
Choice: ").strip().lower()

        if choice == "q":
            save_tasks(tasks)
            print("Saved. Goodbye.")
            break
        elif choice == "a":
            print(add_task(tasks, input("Task: ")))
        elif choice == "d":
            print(toggle_task(tasks, ask_number("Number: ")))
        elif choice == "r":
            print(remove_task(tasks, ask_number("Number: ")))
        else:
            print("Unknown choice.")

        save_tasks(tasks)

main()

How the code works#

  • Saving after every change means an unexpected crash never loses data.
  • enumerate(tasks, start=1) gives human-friendly numbering in one step.
  • sum(1 for t in tasks if t["done"]) counts matching items without a loop body.
  • ask_number returning -1 on bad input lets the validation live in one place.
  • json.dump(..., indent=2) writes a file you can open and read yourself.

Questions people ask#

Why JSON rather than a plain text file?

JSON keeps the structure. A text file would need you to invent and parse your own format for the done flag, which is exactly the work JSON does for you.

Where is tasks.json saved?

In the folder you ran the command from, not necessarily where the script lives. Run import os; print(os.getcwd()) if it appears somewhere unexpected.

Could I use a database instead?

Yes, and Python includes sqlite3. JSON is the right tool at this size; move to SQLite when you have thousands of records or need to search them.

Where to go next#

Next projectBuild a browser to-do list

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *