Build Daily Wins Task Tracker

Build a Python terminal task tracker that saves tasks between sessions.

Introduction

30 Second Summary

A task can feel easy to remember until the day fills up. A useful list keeps your next win visible after the moment passes.

In this project, you will build Daily Wins as a Python terminal app. It tracks your tasks across sessions without crashing on common input mistakes.

What You'll Build

Picture reopening Daily Wins in the IDLE Shell to find yesterday's tasks waiting in a numbered list.

By the end of this project, you'll have:

  • A numbered task list that displays each title as soon as you add it.
  • A reliable completion flow that marks selected work as done while guiding you past blank titles or invalid task numbers.
  • Persistent progress that remains available whenever you reopen Daily Wins.
  • Secret Mission: A safe delete option that removes a selected task while saving the updated list.

Are there any prerequisites?

You do not need prior coding experience. A Windows 10 or newer computer is the only requirement.

The guide includes the Python 3.15.0 plus IDLE setup before coding begins.

Before We Start

A task tracker becomes useful when it holds something you genuinely want to finish. Before the hands-on work begins, choose one real task for Daily Wins to remember and connect it to why completing it matters to you.

Set Up Python and IDLE

Your task now has a purpose. Your computer still needs a verified interpreter before Daily Wins can run.

In this step, you will install Python 3.15.0. You will use IDLE to create your first project file.

In this step, get ready to:
  • Match the Python installer to your Windows architecture.
  • Install Python 3.15.0 with IDLE enabled.
  • Run a saved daily_wins.py starter program in IDLE.
Check your Windows version and architecture

Python 3.15.0 supports Windows 10 and newer. Its Windows installers target different processor architectures.

Finding the architecture can feel fiddly because Windows places it below the main device details. The About page keeps both values together.

  • Right-click Start on the Windows taskbar.
  • Select Settings from the menu.
  • Select System in the Settings window.
  • Select About from the System options.
  • Check Windows specifications to confirm the edition is Windows 10 or newer.
  • Read System type under Device specifications.
  • Record the listed architecture here: your Windows architecture.

Match the Architecture

  • An x64-based processor uses Download Windows installer (64-bit).
  • An x86-based processor uses Download Windows installer (32-bit).
  • An ARM-based processor uses Download Windows installer (ARM64).

Good progress. Your browser's downloads area now contains the installer that matches your computer.

Install Python with IDLE

The full Python installer includes Tcl/Tk support for IDLE by default. The version check below covers the three common starting points.

  • Press the Windows key to open the search box.
  • Type IDLE into the search box.
  • Compare your search result with the three states below.

✔️ I already have Python 3.15.0

Your required Python version is already installed. Opening IDLE confirms that its editor support is available.

  • Select the IDLE search result.

You should see the IDLE Shell. Its heading should identify Python 3.15.0.

ⓧ I see an older Python version

The existing version is too old for this project. The downloaded installer adds the required Python version.

  • Select the downloaded Python installer in your browser's downloads area.
  • Approve the Windows permission request if it appears.
  • Leave the default Tcl/Tk and IDLE feature enabled.
  • Select Install Now in the installer.
  • Close the installer after it confirms completion.
  • Press the Windows key to reopen the search box.
  • Type IDLE into the search box.
  • Select the newest IDLE search result.

You should see the IDLE Shell. Its heading should identify Python 3.15.0.

ⓧ IDLE is not installed

This is the expected starting point on a computer without Python. The installer supplies the interpreter plus IDLE.

  • Select the downloaded Python installer in your browser's downloads area.
  • Approve the Windows permission request if it appears.
  • Leave the default Tcl/Tk and IDLE feature enabled.
  • Select Install Now in the installer.
  • Close the installer after it confirms completion.
  • Press the Windows key to reopen the search box.
  • Type IDLE into the search box.
  • Select the IDLE search result.

You should see the IDLE Shell. Its heading should identify Python 3.15.0.

Cannot Find IDLE After Installing?

  • Run the downloaded Python installer again.
  • Confirm that Tcl/Tk and IDLE support remains enabled.
  • Finish the installer before searching for IDLE again.

Ask for help with the installation details that you can see on your screen.

Create and run the starter file

IDLE gives you an editor for writing code. Its Shell displays the output produced by that code.

  • Select File in the IDLE Shell menu.
  • Select New File to create an editor window.
  • Add the Daily Wins starter message by entering this code:
print("Daily Wins is ready.")

What Does This Code Do?

  • The print() function sends a value to the IDLE Shell.
  • The quoted string becomes the exact message that you see when the file runs.
  • Select File in the editor menu.
  • Select Save As….
  • Choose your Desktop as the save location.
  • Enter daily_wins.py in the file name field.
  • Confirm the save.

Before you run the file, what message do you expect the IDLE Shell to display?

  • Select Run in the editor menu.
  • Select Run Module.

You should see Daily Wins is ready. in the IDLE Shell. That first successful run proves your interpreter, editor, saved file, and Shell are working together.

Do Not See the Starter Message?

  • Confirm that the editor tab is named daily_wins.py.
  • Check that both quotation marks surround the starter message.
  • Run the file again with Run Module.

Get help finding the exact difference in your starter file.

✔️ Awesome, I've got everything!

Your saved starter file is running successfully in the IDLE Shell.

ⓧ I'd like to double check the full code

Compare your saved daily_wins.py file with this complete starter code.

print("Daily Wins is ready.")

Your Python workspace is ready. Next, you will replace the starter message with a task that moves from user input into visible output.

Capture Your First Task

Your Python setup now runs daily_wins.py in the IDLE Shell. That first output proves your editor can run a saved program.

A one-task version gives you a quick result before menus or files add complexity. This step moves text from keyboard input into a variable.

An f-string then displays the stored value. You will see your own task move through the program.

In this step, get ready to:
  • Capture the text returned by input() inside task.
  • Display the stored task with an f-string.
  • Run the program with multiple task titles.
Capture a task with input

The input() function displays a prompt in the Shell. The text you enter becomes a value that the program can store.

  • Switch back to the daily_wins.py editor window from the previous step.
  • Find this starter line:
print("Daily Wins is ready.")

What does the starter line do?

This line prints the same fixed message every time the program runs. It cannot collect a task from you.

  • Replace the starter line with this input line:
task = input("Task title: ")

What does this code do?

  • The input() function displays Task title: in the Shell.
  • The function pauses until you enter a title.
  • The task variable stores the text you enter.
  • Save daily_wins.py.
  • Click Run in the IDLE editor menu bar.
  • Select Run Module.

You will see Task title: in the IDLE Shell. The program is waiting for your input.

  • Enter Finish my first Python project at the prompt.

The Shell accepts your title. The program then finishes because it has no instruction to display the stored value.

Still seeing the starter message?

Make sure you saved daily_wins.py after replacing the starter line. Confirm that you selected Run Module from the editor window.

help me find out why IDLE still runs the previous version of daily_wins.py.

Display the stored task

The task variable now holds your title. An f-string can place that value inside a message.

  • Find the current input line in daily_wins.py:
task = input("Task title: ")

Why find this line?

This line is the point where the program receives your task. The display line belongs directly below it because it uses the stored value.

  • Add the display line beneath the input line so the file matches this code:
task = input("Task title: ")
print(f"Your task: {task}")

How does the task move?

  • The first line stores your entered title in task.
  • The second line places the stored value where {task} appears.
  • The f before the opening quote tells Python to evaluate the value inside the braces.
  • Save daily_wins.py.

Before you run the updated program, what do you expect to appear after you enter a task title?

  • Click Run in the IDLE editor menu bar.
  • Select Run Module.
  • Enter Finish my first Python project at the prompt.

You will see Your task: Finish my first Python project in the Shell. Your typed title now travels from input to output.

Task title missing from the output?

Check that the second line uses task inside curly braces. Confirm that the string begins with f before the opening quote.

help me fix my input and f-string code in daily_wins.py.

✔️ Awesome, I've got everything!

Great. Make sure you saved daily_wins.py before testing more titles.

ⓧ I'd like to double check the full code

Your complete daily_wins.py file should match this code exactly.

task = input("Task title: ")
print(f"Your task: {task}")
Test two task titles

Each run creates a fresh value for task. Testing different titles proves that the output follows your input.

  • Click Run in the IDLE editor menu bar.
  • Select Run Module.
  • Enter Plan tomorrow at the prompt.

You will see Your task: Plan tomorrow in the Shell.

Before the final run, do you expect the Shell to reuse the previous title or display the new one?

  • Click Run in the IDLE editor menu bar.
  • Select Run Module.
  • Enter Finish my first Python project at the prompt.

You will see Your task: Finish my first Python project in the Shell. That confirms your program captures a new title during every run.

Your tracker now captures a task from the keyboard. Next, you will turn one task into a repeating menu that manages a full list.

Build a Repeating Task Menu

Your one-task version accepts text in the IDLE Shell. It exits after displaying that single task.

This step turns the script into a repeating menu. A Python list will hold task dictionaries during each run. A function will display their titles and completion markers.

In this step, get ready to:
  • Replace the one-task script with an in-memory task tracker.
  • Use a repeating menu for View, Add, Complete, and Quit choices.
  • Restart the tracker to test what happens to its task list.
Build the in-memory tracker

Each task needs a title plus a completion state. The tracker stores those two values together inside a dictionary. A repeating while loop keeps the menu available until you choose to quit.

The replacement is split into two paste-sized parts. Together they form the complete in-memory tracker.

  • Replace all existing code in daily_wins.py with this first part:
def show_tasks(tasks):
    print("\nDAILY WINS")
    if not tasks:
        print("No tasks yet.")
        return

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


def main():
    tasks = []

    while True:
        print("\n1. View Tasks")
        print("2. Add Task")
        print("3. Complete Task")
        print("4. Quit")
        choice = input("Choose an option: ").strip()

        if choice == "1":
            show_tasks(tasks)

What does this first part do?

  • The show_tasks() function displays the task list in one consistent format.
  • The enumerate() call gives each task a visible number starting at 1.
  • The marker variable displays [ ] for an unfinished task. It displays [x] for a completed task.
  • The main() function creates the task list. Its loop prints the menu after every completed action.
  • Complete the replacement by pasting this second part directly beneath the first part:
        elif choice == "2":
            title = input("Task title: ").strip()
            tasks.append({"title": title, "done": False})
            print(f"Added: {title}")

        elif choice == "3":
            show_tasks(tasks)
            if not tasks:
                continue

            task_number = input("Task number to complete: ").strip()
            task_index = int(task_number) - 1
            tasks[task_index]["done"] = True
            print(f"Completed: {tasks[task_index]['title']}")

        elif choice == "4":
            print("Goodbye.")
            break

        else:
            print("Choose 1, 2, 3, or 4.")


if __name__ == "__main__":
    main()

How do the menu choices work?

  • The choice variable stores the menu number entered in the Shell.
  • The Add branch stores each title with a done value of False.
  • The Complete branch converts the visible task number into the zero-based task_index used by the list.
  • The __name__ guard calls main() when IDLE runs the file.
  • Save daily_wins.py.
  • Select Run from the IDLE menu bar.
  • Select Run Module.

You'll see a numbered menu with View Tasks, Add Task, Complete Task, and Quit choices. The tracker now waits for your first menu selection.

Does the menu fail to appear?

Check that the second part begins at the same indentation level as the first elif branch. Every branch inside the loop needs matching indentation.

Confirm that the two lines from the earlier version are gone. The file should begin with def show_tasks(tasks):.

help me find the indentation problem in daily_wins.py.

✔️ Awesome, I've got everything!

Your daily_wins.py file now contains the complete in-memory tracker.

ⓧ I'd like to double check the full code

  • Compare daily_wins.py with this reference.
def show_tasks(tasks):
    print("\nDAILY WINS")
    if not tasks:
        print("No tasks yet.")
        return

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


def main():
    tasks = []

    while True:
        print("\n1. View Tasks")
        print("2. Add Task")
        print("3. Complete Task")
        print("4. Quit")
        choice = input("Choose an option: ").strip()

        if choice == "1":
            show_tasks(tasks)

        elif choice == "2":
            title = input("Task title: ").strip()
            tasks.append({"title": title, "done": False})
            print(f"Added: {title}")

        elif choice == "3":
            show_tasks(tasks)
            if not tasks:
                continue

            task_number = input("Task number to complete: ").strip()
            task_index = int(task_number) - 1
            tasks[task_index]["done"] = True
            print(f"Completed: {tasks[task_index]['title']}")

        elif choice == "4":
            print("Goodbye.")
            break

        else:
            print("Choose 1, 2, 3, or 4.")


if __name__ == "__main__":
    main()
Exercise every menu choice

A menu becomes useful when each choice changes or reveals the same task list. You will now add two tasks and complete one of them.

  • Enter 2 at the Choose an option: prompt.
  • Enter Finish my first Python project at the Task title: prompt.

You'll see Added: Finish my first Python project. The menu then returns automatically.

  • Enter 2 at the menu prompt.
  • Enter Review Python notes at the task title prompt.

You'll see Added: Review Python notes. Both task dictionaries now live in the same list.

  • Enter 1 at the menu prompt.

You'll see both tasks in a numbered list. Each one has an empty [ ] completion marker.

  • Enter 3 at the menu prompt.
  • Enter 1 at the task number prompt.

You'll see Completed: Finish my first Python project. The first dictionary now has a done value of True.

  • Enter 1 at the menu prompt.

You'll see 1. [x] Finish my first Python project above the unfinished second task.

  • Enter 4 at the menu prompt.

You'll see Goodbye. before the program stops. Every menu choice has now produced a visible result.

Restart and test the task list

The tracker now handles all four choices during one session. A restart tests whether its task list exists beyond that running copy of the program.

Before you restart, do you think the two tasks and the completed marker will still appear?

  • Select Run from the IDLE menu bar.
  • Select Run Module.
  • Enter 1 at the new menu prompt.

You'll see No tasks yet. The empty list is the intended shortfall in this version.

Why did the tasks disappear?

The line tasks = [] creates a fresh empty list whenever main() starts. The dictionaries exist only in memory during that run.

Run Module restarts the program. The previous in-memory list disappears when that running copy ends.

That's the core interaction loop working. Next, you'll make its tasks survive a restart.

Make Tasks Survive a Restart

Your menu managed several tasks during one run. Restarting erased every task because the list existed only in memory.

This step gives the task list durable storage through JSON persistence.

Each successful change updates tasks.json. A new run then rebuilds the list.

In this step, get ready to:
  • Load saved tasks from a local JSON file.
  • Save every successful Add or Complete action.
  • Restart the tracker to prove that your tasks survive.
Load tasks from JSON

A list in memory disappears when the program closes. A loader gives each new run a task list from the saved file.

  • In the existing IDLE editor for daily_wins.py, place the cursor above def show_tasks(tasks):.
  • Add the JSON import plus the loader by copying this code at the top of daily_wins.py:
import json


DATA_FILE = "tasks.json"


def load_tasks():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []

What does this code add?

  • import json gives the program access to Python's JSON tools.
  • DATA_FILE keeps the data filename in one place.
  • load_tasks() opens the UTF-8 data file before decoding its JSON content.
  • FileNotFoundError returns an empty list when the tracker has no data file yet.
  • Save daily_wins.py.
  • Select Run from the IDLE menu bar.
  • Select Run Module.

You should see the numbered Daily Wins menu. This confirms the new loader has valid syntax.

  • Enter 4 at the menu prompt.
  • Check the loader from the IDLE Shell by entering this call:
load_tasks()

Why Is the Result Empty?

The function looks for tasks.json. No saved change exists yet, so FileNotFoundError returns an empty list.

You should see [] in the Shell. Your tracker can now start safely before its first data file exists.

Loader Not Working?

Check that import json sits at the top of daily_wins.py. Make sure load_tasks() is outside show_tasks().

Check that the lines inside try and except use consistent indentation.

Help me fix the JSON loader in daily_wins.py.

Save every successful change

Loading restores the previous state. Saving captures the current state whenever a task changes.

  • Place the cursor below the closing return [] line in load_tasks().
  • Add the saving function by copying this code before show_tasks():
def save_tasks(tasks):
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(tasks, file, indent=2)

What Does the Saver Do?

  • open() prepares tasks.json for UTF-8 text output.
  • json.dump() converts the task list into JSON before writing it to the file.
  • indent=2 gives the saved data readable indentation.
  • Save daily_wins.py.
  • Select Run from the IDLE menu bar.
  • Select Run Module.

You should see the numbered menu again. The new saving function is defined without stopping the program.

  • Enter 4 to return to the Shell.

Menu Missing After the Edit?

Check the Shell for the line number where Python stopped. Make sure save_tasks() starts at the left edge of the editor.

Help me fix the save_tasks function.

The saver now needs to run immediately after a task enters the list. This creates the data file during the first successful Add action.

  • Find the branch that begins with elif choice == "2": in main().
  • Replace that Add branch with this version:
        elif choice == "2":
            title = input("Task title: ").strip()
            tasks.append({"title": title, "done": False})
            save_tasks(tasks)
            print(f"Added: {title}")

How Does Add Become Persistent?

tasks.append() changes the list in memory. save_tasks(tasks) immediately writes that updated list to tasks.json.

  • Save daily_wins.py.
  • Select Run from the IDLE menu bar.
  • Select Run Module.
  • Enter 2 at the menu prompt.
  • Enter Plan tomorrow at the task prompt.

You should see Added: Plan tomorrow in the Shell. The task is now written to disk.

  • Enter 4 at the menu prompt.
  • Use IDLE's file-opening dialog to open tasks.json from the folder containing daily_wins.py.

You should see a JSON array containing the Plan tomorrow task. Its done value shows that the task is incomplete.

Cannot Find tasks.json?

Check that save_tasks(tasks) appears immediately after tasks.append(). Confirm that daily_wins.py was saved before you used Run Module.

Help me find why tasks.json was not created.

Restart and prove persistence

The tracker now writes new tasks to disk. It must also load that saved list when main() begins.

Completed tasks also change the list. Saving after that change keeps the completed marker across restarts.

  • Switch back to the daily_wins.py editor from earlier.
  • Find the final two lines of the Complete branch.
  • Replace those two lines with this version:
            tasks[task_index]["done"] = True
            save_tasks(tasks)
            print(f"Completed: {tasks[task_index]['title']}")

How Does Completion Stay Saved?

The first line changes the selected task's done value in memory. The next line writes that updated value to tasks.json.

  • Find tasks = [] at the start of main().
  • Replace that line with this loader call:
    tasks = load_tasks()

How Does the Restart Loop Close?

main() now begins with the task list returned by load_tasks(). Every new run starts from the last saved state.

  • Save daily_wins.py.

Before you restart the tracker, consider whether Plan tomorrow will return or the list will reset.

  • Select Run from the IDLE menu bar.
  • Select Run Module.
  • Enter 1 at the menu prompt.

You should see Plan tomorrow in the numbered list. The tracker has restored a task from its previous run.

  • Enter 3 at the menu prompt.
  • Enter 1 as the task number.

You should see Completed: Plan tomorrow in the Shell.

  • Enter 4 at the menu prompt.

Before the final restart, consider whether the completed marker will survive too.

  • Select Run from the IDLE menu bar.
  • Select Run Module.
  • Enter 1 at the menu prompt.

You should see Plan tomorrow with an [x] marker. Both the task and its completed state survived the restart.

  • Switch back to the tasks.json file in IDLE.

The saved task dictionary now shows that its done value is enabled. This connects the completed marker in the Shell to the data stored on disk.

Saved Task Missing After Restart?

Check that DATA_FILE is spelled the same way in both file functions. Confirm that main() starts with tasks = load_tasks().

Check that each save_tasks(tasks) call is indented inside its matching menu branch.

Help me debug why Daily Wins forgets tasks after a restart.

  • Choose the matching tab to confirm your completed daily_wins.py file.

✔️ Awesome, I've got everything!

Your tracker now loads tasks at startup. It saves every successful Add or Complete action.

ⓧ I'd like to double check the full code

Compare your saved daily_wins.py file with this complete version.

import json


DATA_FILE = "tasks.json"


def load_tasks():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []


def save_tasks(tasks):
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(tasks, file, indent=2)


def show_tasks(tasks):
    print("\nDAILY WINS")
    if not tasks:
        print("No tasks yet.")
        return

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


def main():
    tasks = load_tasks()

    while True:
        print("\n1. View Tasks")
        print("2. Add Task")
        print("3. Complete Task")
        print("4. Quit")
        choice = input("Choose an option: ").strip()

        if choice == "1":
            show_tasks(tasks)

        elif choice == "2":
            title = input("Task title: ").strip()
            tasks.append({"title": title, "done": False})
            save_tasks(tasks)
            print(f"Added: {title}")

        elif choice == "3":
            show_tasks(tasks)
            if not tasks:
                continue

            task_number = input("Task number to complete: ").strip()
            task_index = int(task_number) - 1
            tasks[task_index]["done"] = True
            save_tasks(tasks)
            print(f"Completed: {tasks[task_index]['title']}")

        elif choice == "4":
            print("Goodbye.")
            break

        else:
            print("Choose 1, 2, 3, or 4.")


if __name__ == "__main__":
    main()

That restart proves Daily Wins now remembers changes between sessions. Next, you'll protect that durable data from blank titles and invalid task numbers.

Protect the Tracker from Bad Input

Your tracker now reloads saved tasks from JSON. Your progress survives a restart.

The current menu works with perfect input. Blank titles create confusing tasks.

Invalid selections can stop the app. You will add input checks plus exception handling so each problem returns you to a usable menu.

In this step, get ready to:
  • Reject task titles that become empty after .strip().
  • Validate completion choices before using them as list positions.
  • Recover from an unreadable tasks.json file.
Reject blank task titles

The existing .strip() call removes whitespace from both ends of a title. A title containing only spaces becomes an empty string.

  • In the IDLE editor from earlier, locate the Add branch in daily_wins.py.
  • Confirm you found the current Add branch by comparing it with this reference:
        elif choice == "2":
            title = input("Task title: ").strip()
            tasks.append({"title": title, "done": False})
            save_tasks(tasks)
            print(f"Added: {title}")

Why does a blank task get saved?

The current branch saves every stripped title. An empty string reaches tasks.append() because no condition stops it.

  • Replace the current Add branch with this guarded version:
        elif choice == "2":
            title = input("Task title: ").strip()
            if not title:
                print("Task title cannot be empty.")
                continue

            tasks.append({"title": title, "done": False})
            save_tasks(tasks)
            print(f"Added: {title}")

What does this check do?

  • The if not title: condition detects an empty string.
  • The message explains why the title was rejected.
  • The continue statement starts the next menu cycle before the empty title can be saved.

Before you run the updated branch, do you expect a blank title to appear in the task list?

  • Save daily_wins.py.
  • Choose Run Module from the Run menu.
  • Choose option 2.
  • Submit an empty task title by pressing Enter at the Task title: prompt.

You will see Task title cannot be empty. The numbered menu returns immediately.

  • Choose option 1 to inspect the task list.

You will see only your previously saved tasks. No blank item appears.

Is a blank task still appearing?

Check that continue is indented inside if not title:. Confirm that tasks.append() remains below the completed condition.

help me find why my blank title check still saves an empty task.

Validate completion choices

The Complete branch currently converts every response with int(). Text cannot be converted this way.

A number outside the list can also point to a position that does not exist. The tracker needs to check both cases before changing a task.

  • Locate the Complete branch in daily_wins.py.
  • Confirm its current selection logic matches this reference:
            task_number = input("Task number to complete: ").strip()
            task_index = int(task_number) - 1
            tasks[task_index]["done"] = True
            save_tasks(tasks)
            print(f"Completed: {tasks[task_index]['title']}")

Where can this logic fail?

The int(task_number) conversion assumes the response contains digits. A word causes the program to stop.

The list update assumes the calculated position exists. A large number points beyond the available tasks.

  • Replace the current selection logic with this validated version:
            task_number = input("Task number to complete: ").strip()
            if not task_number.isdigit():
                print("Enter a whole-number task choice.")
                continue

            task_index = int(task_number) - 1
            if task_index < 0 or task_index >= len(tasks):
                print("That task number is not in the list.")
                continue

            tasks[task_index]["done"] = True
            save_tasks(tasks)
            print(f"Completed: {tasks[task_index]['title']}")

How is the selection protected?

  • The task_number.isdigit() check confirms the response contains only digits before int() uses it.
  • The subtraction converts the visible task number into a zero-based list position.
  • The bounds check rejects positions below zero or at least as large as len(tasks).
  • Each continue returns to the menu without changing or saving a task.

Before you test the first guard, do you think hello can reach the int() conversion?

  • Save daily_wins.py.
  • Choose Run Module from the Run menu.
  • Choose option 3.
  • Enter hello at the completion prompt.

You will see Enter a whole-number task choice. The menu returns without a crash.

Now predict whether 999 can reach a task inside your saved list.

  • Choose option 3 again.
  • Enter 999 at the completion prompt.

You will see That task number is not in the list. The menu returns again.

  • Choose option 1 to view your tasks.

Your saved tasks still appear with their previous completion states. Neither invalid choice altered them.

Does the program still stop?

Confirm the .isdigit() check appears before int(task_number). Confirm the bounds check appears before tasks[task_index].

help me debug the completion choice validation.

Recover from unreadable JSON

A damaged tasks.json file cannot be decoded into the task list. The loader can catch json.JSONDecodeError and start a fresh in-memory list.

  • Locate load_tasks() near the top of daily_wins.py.
  • Confirm the function currently handles a missing file with this code:
def load_tasks():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []

What is already protected?

The existing FileNotFoundError handler covers the first run before tasks.json exists. Unreadable JSON raises a different exception.

  • Replace load_tasks() with this completed version:
def load_tasks():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError:
        print("Could not read tasks.json. Starting with an empty task list.")
        return []

How does recovery work?

  • The new except block handles saved text that cannot be decoded as JSON.
  • The printed message tells you why the tracker opened with an empty list.
  • The returned empty list keeps the menu available for the current session.

You can test this recovery by temporarily making the saved file unreadable. You will restore the file before making another saved change.

  • Save daily_wins.py.
  • Open tasks.json in the IDLE editor from the same folder as daily_wins.py.
  • Remove the final closing square bracket from tasks.json.
  • Save tasks.json.

Before you run the tracker, do you expect the unreadable file to stop the menu from loading?

  • Switch back to daily_wins.py.
  • Choose Run Module from the Run menu.

You will see Could not read tasks.json. Starting with an empty task list. The numbered menu still appears.

  • Return to tasks.json.
  • Undo the bracket deletion with the editor's undo action.
  • Save the restored tasks.json file.
  • Switch back to daily_wins.py.
  • Choose Run Module from the Run menu.
  • Choose option 1.

Your saved tasks appear again. This confirms the tracker can recover from unreadable data without overwriting the restored file.

Is the saved list still empty?

Confirm the closing square bracket is back in tasks.json. Save the restored file before running daily_wins.py again.

help me restore my tasks.json file after testing JSON error recovery.

✔️ Awesome, I've got everything!

Your title check, completion checks, and JSON recovery handler are now in place.

  • Confirm daily_wins.py is saved.

ⓧ I'd like to double check the full code

Compare your saved file with the complete protected tracker below.

import json


DATA_FILE = "tasks.json"


def load_tasks():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError:
        print("Could not read tasks.json. Starting with an empty task list.")
        return []


def save_tasks(tasks):
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(tasks, file, indent=2)


def show_tasks(tasks):
    print("\nDAILY WINS")
    if not tasks:
        print("No tasks yet.")
        return

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


def main():
    tasks = load_tasks()

    while True:
        print("\n1. View Tasks")
        print("2. Add Task")
        print("3. Complete Task")
        print("4. Quit")
        choice = input("Choose an option: ").strip()

        if choice == "1":
            show_tasks(tasks)

        elif choice == "2":
            title = input("Task title: ").strip()
            if not title:
                print("Task title cannot be empty.")
                continue

            tasks.append({"title": title, "done": False})
            save_tasks(tasks)
            print(f"Added: {title}")

        elif choice == "3":
            show_tasks(tasks)
            if not tasks:
                continue

            task_number = input("Task number to complete: ").strip()
            if not task_number.isdigit():
                print("Enter a whole-number task choice.")
                continue

            task_index = int(task_number) - 1
            if task_index < 0 or task_index >= len(tasks):
                print("That task number is not in the list.")
                continue

            tasks[task_index]["done"] = True
            save_tasks(tasks)
            print(f"Completed: {tasks[task_index]['title']}")

        elif choice == "4":
            print("Goodbye.")
            break

        else:
            print("Choose 1, 2, 3, or 4.")


if __name__ == "__main__":
    main()

For the final check, do you expect any invalid entry to change your saved task list?

  • Click the Run menu in the daily_wins.py editor.
  • Click Run Module.
  • Choose option 2.
  • Submit a blank title by pressing Enter.
  • Choose option 3.
  • Enter hello.
  • Choose option 3 again.
  • Enter 999.

You will see Task title cannot be empty., Enter a whole-number task choice., and That task number is not in the list. Each message is followed by the menu.

  • Choose option 1 to inspect the saved list.

Your saved tasks remain available with no blank entry or accidental completion. You did it. Daily Wins now handles common mistakes while preserving the work that matters.

Secret mission

Delete a Task Safely

Completed tasks and mistaken entries can crowd your tracker. Add a safe Delete choice that validates the selected task and keeps it removed after restart.

Clean Up Your Resources

Clean Up Your Resources

Daily Wins runs entirely on your Windows computer with no ongoing costs. Decide whether to keep both files, close IDLE to pause, or delete the files entirely.

Resources you used:

  • The local daily_wins.py program. It contains the Python code for your task tracker.
  • The local tasks.json data file. It stores your current task titles plus their completion states as JSON.

Keep everything running

No action is needed. Choose this if you want to continue using or improving Daily Wins.

  • Leave daily_wins.py beside tasks.json in the folder where you saved them.
  • Run daily_wins.py from IDLE whenever you want to manage your tasks.
  • Keep Python 3.15.0 plus IDLE installed for this tracker or future Python projects.

Pause - I'll come back to this later

Close the running app to pause your work while keeping your program and saved tasks available.

  • Return to the IDLE Shell from earlier.
  • Enter 5 if the Daily Wins menu is still running.
  • Close IDLE.
  • Leave daily_wins.py beside tasks.json so the tracker can restore your saved tasks next time.

Delete - I don't want to use this again

Remove both local files if you want to start fresh. Your Python and IDLE installation stays available for another project.

  • Return to the IDLE Shell from earlier.
  • Enter 5 if the Daily Wins menu is still running.
  • Close IDLE.
  • In File Explorer, go to the folder that contains daily_wins.py plus tasks.json.
  • Remove daily_wins.py from that folder.
  • Remove tasks.json from that folder.
  • Confirm that neither filename remains in the folder.

Your Daily Wins program and its saved task data are now removed from the computer.

Nice Work!

Nice Work!

You did it! You built Daily Wins, a persistent terminal task tracker with Python. Your app remembers progress across sessions.

You've learned how to:

  • Built a menu-driven Python application that adds tasks. It displays numbered status markers. It completes selected tasks.
  • Stored task dictionaries in a list. Used JSON persistence to save that list in tasks.json. The app restores the saved data when it starts.
  • Added input validation for blank titles. Added checks for invalid task numbers. Used exception handling to recover from unreadable JSON.
  • Completed the Secret Mission by adding safe task deletion. Deleted tasks stay removed after restart.

Ready to quiz yourself?