Skip to Content
Course content

411: Building a Command-Line To-Do App

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time building massive enterprise systems, but honestly, some of the best learning happens when you just try to build a tool you'll actually use. Today, we're tackling a CLI To-Do app. It sounds simple, but it's the perfect playground for handling state and file I/O.

The Memory Reset Problem

My first instinct is usually to just throw everything into a list. Let's try that. I'll start a script, create a list called tasks, and let the user add something to it.

tasks = []
while True:
    item = input("Add a task (or type 'done'): ")
    if item == 'done':
        break
    tasks.append(item)

print(f"Your list: {tasks}")

This works great while the script is running. I can add "Buy milk" and "Fix the leaky faucet," and the list grows. But here is the problem: the second I hit 'done' and the program exits, my list vanishes. If I restart the script, I'm back to square one. That's not a tool; that's a temporary notepad. I need the data to live somewhere outside the Python process.

Fighting the Memory Reset

The obvious fix is a text file. I'll try writing the list to a file every time I add an item. I'll use a simple .txt file where each task gets its own line.

# Let's try writing the list to a file
with open("todo.txt", "w") as f:
    for task in tasks:
        f.write(task + "\n")

Wait, I just realized a flaw in my logic. If I use "w" (write mode), Python wipes the file clean every time I open it. If I want to keep adding tasks without loading the whole list into memory first, I should use "a" for append. But then I have a different problem: how do I read those tasks back into the program when it starts up? I can't just read() the whole thing as one giant string.

I'll try readlines(). It gives me a list of strings, which is exactly what I want. One annoying detail, though—readlines() keeps the newline character \n at the end of every string. If I print my list now, every item is on a new line with an extra gap. I'll use a list comprehension with .strip() to clean that up.

with open("todo.txt", "r") as f:
    tasks = [line.strip() for line in f.readlines()]

The Indexing Headache

Now that we can save and load, we need a way to remove tasks. I thought about just letting the user type the name of the task to delete it, but what if they have two tasks called "Clean room"? That's a recipe for deleting the wrong thing. Using an index (a number) is safer.

I tried this quickly: del tasks[int(input("Enter number to delete: "))]. It crashed immediately. Why? Because I typed "1" to delete the first item, but Python lists start at 0. I tried typing "0", and it worked, but no normal human wants to start counting their to-do list from zero. It feels unnatural.

The fix is a simple offset. I'll tell the user the list starts at 1, then subtract 1 from their input before passing it to the list. It's a small UX tweak, but it's the difference between a tool that feels broken and one that feels professional.

Tying the Loop Together

I've realized that having separate blocks of code for adding and deleting is messy. I need a main menu loop that keeps the app alive until I explicitly tell it to quit. I'll use a while True loop and a set of if/elif statements to route the user's choice.

def main():
    # Load existing tasks first
    try:
        with open("todo.txt", "r") as f:
            tasks = [line.strip() for line in f.readlines()]
    except FileNotFoundError:
        tasks = []

    while True:
        print("\n--- TO-DO APP ---")
        print("1. View Tasks\n2. Add Task\n3. Remove Task\n4. Exit")
        choice = input("Choose an option: ")

        if choice == "1":
            for i, task in enumerate(tasks, 1):
                print(f"{i}. {task}")
        elif choice == "2":
            new_task = input("What needs doing? ")
            tasks.append(new_task)
            with open("todo.txt", "w") as f:
                for t in tasks: f.write(t + "\n")
        elif choice == "3":
            idx = int(input("Task number to remove: ")) - 1
            if 0 <= idx < len(tasks):
                tasks.pop(idx)
                with open("todo.txt", "w") as f:
                    for t in tasks: f.write(t + "\n")
            else:
                print("Invalid number!")
        elif choice == "4":
            break

main()

I noticed that I'm repeating the "save to file" logic in both the add and remove sections. That's a smell. In a real project, I'd move that into a save_tasks() function to keep things DRY (Don't Repeat Yourself). But for this exploration, the logic is clear: Load $\rightarrow$ Loop $\rightarrow$ Modify $\rightarrow$ Save.




📋 Practical Task

Build a Priority-Based Task Organizer

Modify the To-Do app we explored in the lesson to support priority levels. Instead of just storing a task name, each task should be stored as a string that includes a priority marker (e.g., "High: Buy Milk" or "Low: Water Plants").

  • Modify the "Add Task" section to ask the user for a priority (High, Medium, or Low) before asking for the task description.
  • Update the "View Tasks" section so that the app attempts to list all "High" priority tasks first, regardless of the order they were added.
  • Ensure the tasks are still saved to and loaded from the todo.txt file so that the priorities persist across sessions.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.