Python To-Do List Application

Python To-Do List Application

Building a to-do list application is a classic way to practice your Python skills. It involves multiple programming concepts: handling user input, storing data, updating state, and displaying information. Let’s build one together from scratch. You'll learn how to create a simple yet functional console-based to-do list app that you can run right in your terminal.

We’ll start by defining the core features. Our app will allow a user to add tasks, remove completed tasks, display the current list, and quit the application. We'll use a Python list to store tasks and a while loop to keep the program running until the user decides to exit.

Let's look at the initial setup. We need an empty list to hold tasks and a way to repeatedly prompt the user for commands. Here’s a skeleton of the main loop:

tasks = []

while True:
    print("\nTo-Do List Menu:")
    print("1. Add task")
    print("2. Remove task")
    print("3. Show tasks")
    print("4. Quit")

    choice = input("Enter your choice: ")

    if choice == "1":
        # Add a task
        pass
    elif choice == "2":
        # Remove a task
        pass
    elif choice == "3":
        # Show tasks
        pass
    elif choice == "4":
        print("Goodbye!")
        break
    else:
        print("Invalid choice. Try again.")

Now, let’s implement the "Add task" functionality. We’ll ask the user for the task description and append it to the tasks list.

if choice == "1":
    task = input("Enter the task: ")
    tasks.append(task)
    print(f"Added: {task}")

The "Show tasks" option is straightforward—we loop through the list and display each task with a number. This helps the user identify which task to remove later.

elif choice == "3":
    if not tasks:
        print("Your to-do list is empty.")
    else:
        print("\nYour tasks:")
        for index, task in enumerate(tasks, start=1):
            print(f"{index}. {task}")

For removing a task, we show the numbered list again and ask the user which task they want to remove. We use try and except to handle invalid inputs gracefully.

elif choice == "2":
    if not tasks:
        print("No tasks to remove.")
    else:
        print("\nCurrent tasks:")
        for index, task in enumerate(tasks, start=1):
            print(f"{index}. {task}")
        try:
            task_num = int(input("Enter task number to remove: "))
            removed_task = tasks.pop(task_num - 1)
            print(f"Removed: {removed_task}")
        except (ValueError, IndexError):
            print("Invalid task number.")

Putting it all together, here’s the full code for our basic to-do list application:

tasks = []

while True:
    print("\nTo-Do List Menu:")
    print("1. Add task")
    print("2. Remove task")
    print("3. Show tasks")
    print("4. Quit")

    choice = input("Enter your choice: ")

    if choice == "1":
        task = input("Enter the task: ")
        tasks.append(task)
        print(f"Added: {task}")
    elif choice == "2":
        if not tasks:
            print("No tasks to remove.")
        else:
            print("\nCurrent tasks:")
            for index, task in enumerate(tasks, start=1):
                print(f"{index}. {task}")
            try:
                task_num = int(input("Enter task number to remove: "))
                removed_task = tasks.pop(task_num - 1)
                print(f"Removed: {removed_task}")
            except (ValueError, IndexError):
                print("Invalid task number.")
    elif choice == "3":
        if not tasks:
            print("Your to-do list is empty.")
        else:
            print("\nYour tasks:")
            for index, task in enumerate(tasks, start=1):
                print(f"{index}. {task}")
    elif choice == "4":
        print("Goodbye!")
        break
    else:
        print("Invalid choice. Try again.")

Here’s a summary of what each menu option does:

  • Add task: Allows the user to input a new task and adds it to the list.
  • Remove task: Displays all tasks with numbers and lets the user remove one by entering its number.
  • Show tasks: Prints all current tasks in a numbered list.
  • Quit: Exits the program.
Menu Option Action Description Input Expected
Add task Appends a new task Task string
Remove task Deletes a task Task number
Show tasks Displays all tasks None
Quit Exits the app None

Now, let’s think about improving our app. One common issue is that the tasks are lost when the program closes. To solve this, we can add functionality to save tasks to a file and load them when the program starts. We’ll use a text file and store each task on a new line.

First, we create a function to load tasks from a file (if it exists):

def load_tasks(filename="todo.txt"):
    try:
        with open(filename, "r") as file:
            return [line.strip() for line in file.readlines()]
    except FileNotFoundError:
        return []

And a function to save the current tasks to the file:

def save_tasks(tasks, filename="todo.txt"):
    with open(filename, "w") as file:
        for task in tasks:
            file.write(task + "\n")

We integrate these into our main program. At the start, we load tasks from the file, and after every change (adding or removing), we save the updated list.

tasks = load_tasks()

while True:
    # ... menu and choices ...

    if choice == "1":
        task = input("Enter the task: ")
        tasks.append(task)
        save_tasks(tasks)
        print(f"Added: {task}")
    elif choice == "2":
        # ... remove task code ...
        save_tasks(tasks)  # Save after removal
    # ... other choices ...

This way, your tasks persist between runs. You can close the program and come back later, and your to-do list will be exactly as you left it.

Another useful enhancement is marking tasks as completed instead of just removing them. We can change our task storage from a simple list of strings to a list of dictionaries, where each dictionary holds the task description and its completion status.

tasks = [
    {"description": "Buy groceries", "completed": False},
    {"description": "Learn Python", "completed": True},
]

We would then update our functions to handle this structure. The "Show tasks" option could display completed tasks with a checkmark, and the "Remove task" option could become "Mark task as completed."

Let’s refactor the display function to show status:

elif choice == "3":
    if not tasks:
        print("Your to-do list is empty.")
    else:
        print("\nYour tasks:")
        for index, task in enumerate(tasks, start=1):
            status = "✓" if task["completed"] else " "
            print(f"{index}. [{status}] {task['description']}")

And add a new option for marking tasks as done:

elif choice == "4":  # New option: mark task as done
    if not tasks:
        print("No tasks to mark.")
    else:
        print("\nCurrent tasks:")
        for index, task in enumerate(tasks, start=1):
            status = "✓" if task["completed"] else " "
            print(f"{index}. [{status}] {task['description']}")
        try:
            task_num = int(input("Enter task number to mark as done: "))
            tasks[task_num - 1]["completed"] = True
            save_tasks(tasks)
            print("Task marked as done.")
        except (ValueError, IndexError):
            print("Invalid task number.")

We also need to update our file saving and loading to handle the new structure. We can use JSON for this, as it supports nested structures like lists of dictionaries.

First, import the json module:

import json

Then, modify the load and save functions:

def load_tasks(filename="todo.json"):
    try:
        with open(filename, "r") as file:
            return json.load(file)
    except FileNotFoundError:
        return []

def save_tasks(tasks, filename="todo.json"):
    with open(filename, "w") as file:
        json.dump(tasks, file)

Now, our to-do list is more powerful. We can see which tasks are done and which are still pending.

Task Property Data Type Example Value
description string "Write blog post"
completed boolean True

Let’s summarize the key steps we took to build and enhance our app:

  • Started with a basic menu-driven console app.
  • Added functionality to add, remove, and display tasks.
  • Implemented file I/O to persist tasks between sessions.
  • Enhanced the data structure to support completion status.
  • Used JSON for more flexible data storage.

Building a to-do list app is a great exercise because it covers many fundamental programming concepts. You practice working with loops, conditionals, lists, functions, and file handling—all in one project. Plus, you end up with a useful tool you can customize further.

If you want to go further, consider adding features like:

  • Due dates for tasks.
  • Prioritization (e.g., high, medium, low).
  • Categories or tags for tasks.
  • A graphical user interface using Tkinter or PyQt.

But even in its simple form, this app is functional and teaches you a lot. Try running the code, adding a few tasks, and see how it works. Experiment with breaking it and fixing it—that’s one of the best ways to learn.

Here’s the full enhanced code with completion status and JSON storage:

import json

def load_tasks(filename="todo.json"):
    try:
        with open(filename, "r") as file:
            return json.load(file)
    except FileNotFoundError:
        return []

def save_tasks(tasks, filename="todo.json"):
    with open(filename, "w") as file:
        json.dump(tasks, file)

tasks = load_tasks()

while True:
    print("\nTo-Do List Menu:")
    print("1. Add task")
    print("2. Remove task")
    print("3. Show tasks")
    print("4. Mark task as done")
    print("5. Quit")

    choice = input("Enter your choice: ")

    if choice == "1":
        task_desc = input("Enter the task: ")
        tasks.append({"description": task_desc, "completed": False})
        save_tasks(tasks)
        print(f"Added: {task_desc}")
    elif choice == "2":
        if not tasks:
            print("No tasks to remove.")
        else:
            print("\nCurrent tasks:")
            for index, task in enumerate(tasks, start=1):
                status = "✓" if task["completed"] else " "
                print(f"{index}. [{status}] {task['description']}")
            try:
                task_num = int(input("Enter task number to remove: "))
                removed_task = tasks.pop(task_num - 1)
                save_tasks(tasks)
                print(f"Removed: {removed_task['description']}")
            except (ValueError, IndexError):
                print("Invalid task number.")
    elif choice == "3":
        if not tasks:
            print("Your to-do list is empty.")
        else:
            print("\nYour tasks:")
            for index, task in enumerate(tasks, start=1):
                status = "✓" if task["completed"] else " "
                print(f"{index}. [{status}] {task['description']}")
    elif choice == "4":
        if not tasks:
            print("No tasks to mark.")
        else:
            print("\nCurrent tasks:")
            for index, task in enumerate(tasks, start=1):
                status = "✓" if task["completed"] else " "
                print(f"{index}. [{status}] {task['description']}")
            try:
                task_num = int(input("Enter task number to mark as done: "))
                tasks[task_num - 1]["completed"] = True
                save_tasks(tasks)
                print("Task marked as done.")
            except (ValueError, IndexError):
                print("Invalid task number.")
    elif choice == "5":
        print("Goodbye!")
        break
    else:
        print("Invalid choice. Try again.")

I hope you enjoyed building this with me. Keep coding and experimenting—every project makes you a better programmer.