Build a Persistent Habit Tracker

Build a Python CLI habit tracker with persistent JSON data.

Introduction

30 Second Summary

Daily habits are easier to maintain when progress stays visible. Losing yesterday's record makes today's effort feel disconnected.

In this project, you will build a menu-driven command-line habit tracker in Python. Its local JSON file preserves valid habits across restarts.

What You'll Build

When you reopen your tracker in Visual Studio Code's terminal, your saved habits are waiting with their latest completion status.

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

  • A numbered habit list where each new habit appears immediately.
  • Completion controls that mark a selected habit done. Progress totals that show how many habits are complete. A reset option that clears every completion status for a fresh day.
  • Persistent local data that keeps valid habits after you close the program. The tracker starts safely when saved data is missing or malformed.
  • Secret Mission: Replace manual daily resets with date-aware streak tracking. Prevent duplicate same-day completions.

Are there any prerequisites?

You need a Mac with Visual Studio Code already installed.

General programming experience in another language is the only coding background you need.

Before We Start

A clear user and first habit give your tracker a purpose before any hands-on work begins. Take a moment to commit to what you are building and why it matters.

Set Up Python in Visual Studio Code

Your Python file needs an interpreter before it can run. Visual Studio Code also needs the Python extension to send your file to the correct interpreter.

This step installs that toolchain on your Mac. You will finish with a one-line program that proves the editor can run Python correctly.

In this step, get ready to:
  • Install Python 3.15.2 with its certificates.
  • Connect the Microsoft Python extension to the traditional interpreter.
  • Create the tracker file and run its first line.
Install Python and certificates

The interpreter turns the instructions in a .py file into a running program. Checking the current version first reveals which installation path your Mac needs.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal into Spotlight.
  • Press Return to open Terminal.

Before you check, do you expect your Mac to find the required Python version?

  • Check the available Python version by running this command:
python3 --version

What does this command check?

  • The python3 command asks your shell to find a Python 3 interpreter.
  • The --version flag prints the interpreter version without starting a program.
  • Choose the tab that matches your Terminal output.

✔️ I see Python 3.15.2

Your Mac already has the required interpreter. Keep this installation for the project.

ⓧ I see an older version

Your Mac is finding an older interpreter. The Python.org installer adds the required version without changing Apple-controlled Python files.

A macOS administrator prompt can feel sensitive, but it only authorizes the signed local installer.

  • Open the Python.org macOS downloads page.
  • Download the macOS .pkg installer for Python 3.15.2.
  • Open the downloaded .pkg from your Downloads folder in Finder.
  • Complete the macOS Installer with its standard options.

ⓧ Command not found

Your shell cannot find a Python interpreter yet. Installing the signed Python.org package adds the command your tracker needs.

A macOS administrator prompt can feel sensitive, but it only authorizes the signed local installer.

  • Open the Python.org macOS downloads page.
  • Download the macOS .pkg installer for Python 3.15.2.
  • Open the downloaded .pkg from your Downloads folder in Finder.
  • Complete the macOS Installer with its standard options.

Finding the certificate helper is a little fiddly because the installer places it in a separate application folder.

  • In Finder, open the /Applications/Python 3.15/ folder.
  • Double-click Install Certificates.command.

Terminal reports Successfully installed certifi. The final line includes update complete.

Python installation not completing?

  • Confirm that you downloaded the Python 3.15.2 macOS installer from Python.org.
  • Leave /usr/bin/python3 unchanged because macOS controls that file.
  • Close Terminal before opening a fresh window after the installation.

If the check still fails, help me troubleshoot my Python 3.15.2 installation on macOS.

Good progress. Your Mac now has the interpreter and certificates needed to run the tracker.

Connect the Python extension

The Python extension adds language support plus the run controls inside Visual Studio Code. It uses a separately installed interpreter to execute your code.

  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code into Spotlight.
  • Press Return to open Visual Studio Code.
  • Follow the Visual Studio Code update prompt if one appears.

Visual Studio Code is now ready for the extension that connects the editor to Python.

  • Select the Extensions icon in the Activity Bar.
  • Enter Python in the Extensions search box.
  • Select the Python extension published by Microsoft.
  • Choose the tab that matches the extension details page.

✔️ I see version 2026.10.0

The required Microsoft Python extension is already installed. Continue with this version.

ⓧ I see an older version

  • Select Update on the Python extension details page.
  • Select Restart Extensions if Visual Studio Code prompts you after the update.

The extension details page shows the updated version after the extension host restarts.

ⓧ The extension is not installed

  • Select Install on the Python extension details page.

The installation control changes to a management control when the extension is ready.

  • Confirm that the extension details page shows version 2026.10.0.

The Python installer provides a traditional build plus a separate free-threaded build. This project uses the traditional interpreter.

The free-threaded interpreter ends in 3.15t. The traditional interpreter appears as Python 3.15.

  • Press Cmd+Shift+P on macOS to open the Command Palette.
  • Type Python: Select Interpreter.
  • Press Return to open the interpreter list.
  • Select the Python 3.15 interpreter whose name does not end in 3.15t.

The selected Python interpreter now appears in the Visual Studio Code status bar. The editor has a runtime for every Python file you run.

Can't find the traditional interpreter?

  • Confirm that the Python 3.15.2 installer completed before reopening Visual Studio Code.
  • Confirm that you installed the Python extension published by Microsoft.
  • Reopen Python: Select Interpreter after restarting Visual Studio Code.

If the interpreter is still missing, help me connect the traditional Python 3.15 interpreter to Visual Studio Code.

The editor and interpreter are connected. You can now prove the setup with the first tracker file.

Create and run the tracker

A Visual Studio Code workspace keeps the tracker file and its future data together. Your workspace lives in a named folder on the Desktop.

Visual Studio Code may ask whether you trust this folder. You can trust it because you are creating its contents yourself.

  • Select File in the Visual Studio Code menu bar.
  • Select Open Folder....
  • Select Desktop in the folder dialog.
  • Select New Folder.
  • Enter python-habit-tracker as the folder name.
  • Confirm the folder creation in the dialog.
  • Select Open to use the folder as your workspace.
  • Select Yes, I trust the authors if the Workspace Trust dialog appears.

The Explorer sidebar now shows python-habit-tracker as the open folder.

  • Select the Explorer icon in the Activity Bar.
  • Select New File... in the Explorer toolbar.
  • Enter habit_tracker.py as the file name.
  • Press Return to create the file.
  • Add the first startup message to habit_tracker.py by pasting this code:
print("Habit Tracker is ready.")

What does this code do?

  • The quoted text holds the exact startup message for your tracker.
  • Python writes that message to the terminal when the file runs.
  • This visible line proves that Visual Studio Code is using the selected interpreter.
  • Press Cmd+S on macOS to save habit_tracker.py.
  • Select Terminal in the Visual Studio Code menu bar.
  • Select New Terminal to open a fresh integrated terminal.

Before you check, do you expect the fresh terminal to report the traditional Python version you selected?

  • Verify the interpreter version by running this command:
python3 --version

What does this verification prove?

The command asks the fresh Visual Studio Code terminal which Python 3 interpreter it can reach. The reported version confirms that your installation is available inside the editor.

The terminal prints Python 3.15.2.

Before you run the file, what startup message do you expect your saved line to print?

  • Select the Run Python File play button in the top-right corner of the editor.

The terminal prints Habit Tracker is ready. beneath the version check. Your first Python file is running through Visual Studio Code.

Tracker output missing?

  • Confirm that habit_tracker.py is the active editor tab.
  • Confirm that the file contains the same capitalization and punctuation as the reference code.
  • Reopen Python: Select Interpreter if the play button uses a different interpreter.

If the output still does not appear, help me troubleshoot why habit_tracker.py is not printing in the Visual Studio Code terminal.

✔️ Awesome, I've got everything!

Great. Double-check that you saved habit_tracker.py inside python-habit-tracker.

ⓧ I'd like to double check the full code

print("Habit Tracker is ready.")

Your Python workspace is ready. Next, you will turn this startup message into an in-memory tracker that can add and list habits.

Build an In-Memory Tracker

Your readiness message proved that Visual Studio Code can run your Python file. Now the tracker needs a menu that holds habits while you use it.

An in-memory list gives that menu an immediate working collection. You will restart the program to test what happens to the collection.

In this step, get ready to:
  • Define actions that add and list habits.
  • Build a menu that responds to numbered choices.
  • Restart the tracker to test whether its habits remain.
Define the habit actions

Functions give each tracker action a focused job. Each habit is a dictionary with a name and a completion state.

  • In habit_tracker.py, select the existing print("Habit Tracker is ready.") line.
  • Replace the selected line by pasting this code:
def list_habits(habits):
    if not habits:
        print("\nNo habits yet.")
        return

    print("\nYour habits:")
    for habit in habits:
        print(habit["name"])


def add_habit(habits):
    name = input("Habit name: ").strip()
    if not name:
        print("Habit name cannot be empty.")
        return

    habits.append({"name": name, "completed": False})
    print(f"Added: {name}")

What do these functions do?

  • The list_habits() function handles an empty collection before trying to display its contents.
  • The for habit in habits: loop visits each habit dictionary.
  • The add_habit() function removes surrounding whitespace from the entered name.
  • The empty-name check prevents a blank habit from entering the collection.
  • The append() call adds a dictionary with name and completed fields.
  • Save habit_tracker.py.
  • Confirm the file begins with the list_habits() definition.
  • Confirm the add_habit() definition appears below it.

Do the functions look misaligned?

Check that each function body uses the same indentation. Make sure both function definitions begin at the far left of the editor.

Compare the quotation marks around each printed message. Help me find an indentation or quotation-mark problem in these Python functions.

Build the interactive menu

A loop keeps the menu available after each action. Conditionals connect every numbered choice to the correct function.

  • Place the cursor on a blank line below the add_habit() function.
  • Add the menu and program entry point by pasting this code:
def main():
    habits = []

    while True:
        print("\nHabit Tracker")
        print("1. List habits")
        print("2. Add habit")
        print("3. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            list_habits(habits)
        elif choice == "2":
            add_habit(habits)
        elif choice == "3":
            print("See you next time.")
            break
        else:
            print("Choose a number from 1 to 3.")


if __name__ == "__main__":
    main()

How does the menu work?

  • The habits list holds the current run's habit dictionaries.
  • The while True: loop displays the menu until the learner chooses to quit.
  • The conditional branches call the matching function for choices 1 and 2.
  • Choice 3 reaches break to end the loop.
  • The final guard starts main() when you run habit_tracker.py directly.
  • Save habit_tracker.py.

Before you run it, which menu response do you expect when the tracker has no habits?

  • Click the Run Python File play button in the top-right corner of the editor.

The terminal displays the Habit Tracker menu with choices numbered from 1 to 3.

  • Enter 1 at the Choose an option: prompt.

You will see No habits yet. before the menu returns.

  • Enter 2 at the menu prompt.
  • Enter Read 10 pages at the habit-name prompt.

You will see Added: Read 10 pages before the menu returns.

  • Enter 1 at the menu prompt.

You will see Read 10 pages under Your habits:. Your in-memory tracker can now add and list a habit.

  • Enter 3 at the menu prompt.

The terminal prints See you next time. and returns to its prompt.

Menu not responding correctly?

Check that main() appears inside the final guard. Confirm that each elif lines up with the first if.

Make sure your entered choices contain only the menu number. Help me troubleshoot my habit tracker menu.

✔️ Awesome, I've got everything!

Great. Double-check that you saved habit_tracker.py.

ⓧ I'd like to double check the full code

def list_habits(habits):
    if not habits:
        print("\nNo habits yet.")
        return

    print("\nYour habits:")
    for habit in habits:
        print(habit["name"])


def add_habit(habits):
    name = input("Habit name: ").strip()
    if not name:
        print("Habit name cannot be empty.")
        return

    habits.append({"name": name, "completed": False})
    print(f"Added: {name}")


def main():
    habits = []

    while True:
        print("\nHabit Tracker")
        print("1. List habits")
        print("2. Add habit")
        print("3. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            list_habits(habits)
        elif choice == "2":
            add_habit(habits)
        elif choice == "3":
            print("See you next time.")
            break
        else:
            print("Choose a number from 1 to 3.")


if __name__ == "__main__":
    main()
Test a fresh run

A restart tests whether the next process can recover the habit from the previous process. This check exposes the current tracker's storage boundary.

Before you run it again, do you expect Read 10 pages to still be listed?

  • Click the Run Python File play button again.
  • Enter 1 at the menu prompt.

You will see No habits yet. even though the menu still works. This is the intended shortfall.

Each call to main() creates a new empty habits list. The previous list existed only in the process that you quit.

  • Enter 3 to quit the fresh run.

Still seeing the old habit?

Confirm that the previous run ended after you entered 3. Start the file again before choosing the list option.

Check that habits = [] remains inside main(). Help me verify that I restarted my in-memory tracker correctly.

You have a working menu with a clear limitation exposed by a fresh run. Next, you will make valid habits survive after the program closes.

Save Habits to JSON

Your tracker now adds habits through a working menu. A restart clears its in-memory list.

A readable JSON file preserves that list between sessions. Python can reconstruct the same habit dictionaries when the tracker starts again.

In this step, get ready to:
  • Load saved habits when the tracker starts.
  • Save each new habit to a readable local file.
  • Prove that a habit survives a full restart.
Load saved habits

JSON serialization turns Python lists and dictionaries into structured text. The loader also treats a missing save file as an empty tracker.

  • Return to habit_tracker.py in Visual Studio Code.
  • Paste the JSON loading block above list_habits(habits) in habit_tracker.py:
import json

DATA_FILE = "habits.json"


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

    return []

What does this code do?

  • The json module provides the tools for reconstructing Python data from JSON.
  • DATA_FILE keeps the save filename in one place.
  • load_habits() reads the saved value from habits.json.
  • isinstance(habits, list) accepts the data only when its top-level shape is a list.
  • FileNotFoundError returns an empty list when the tracker has no save file yet.
  • Save habit_tracker.py.
  • Click the Run Python File play button in the top-right corner of the editor.
  • Enter 1 at the menu prompt.

You will see No habits yet. because no save file exists at this point. The menu keeps running because the loader returned an empty list.

  • Enter 3 to quit the tracker.

Tracker not reaching the menu?

Check that load_habits() appears above list_habits(habits). Make sure every line inside try has the same indentation pattern as the code above.

Confirm that except FileNotFoundError: aligns with try:. Help me troubleshoot my JSON loading function.

Write additions to disk

The tracker needs a matching save function before new habits can persist. The indent=2 setting keeps the resulting JSON readable.

  • Paste save_habits(habits) between load_habits() and list_habits(habits) in habit_tracker.py:
def save_habits(habits):
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(habits, file, indent=2)

How does saving work?

  • The writing mode creates habits.json when the file does not exist.
  • json.dump serializes the current habit list into that file.
  • The file context closes the save file after writing finishes.
  • Replace the existing add_habit(habits) function with this saved version:
def add_habit(habits):
    name = input("Habit name: ").strip()
    if not name:
        print("Habit name cannot be empty.")
        return

    habits.append({"name": name, "completed": False})
    save_habits(habits)
    print(f"Added: {name}")

What changed in this function?

  • The function still appends a dictionary containing name and completed fields.
  • save_habits(habits) writes the updated list immediately after a valid habit is added.
  • Invalid empty names still return before any file change occurs.
  • Save habit_tracker.py.
  • Click the Run Python File play button.
  • Enter 2 at the menu prompt.
  • Enter Walk for 20 minutes as the habit name.

The terminal confirms that the habit was added. The save function also creates habits.json beside the Python file.

  • Enter 3 to quit the tracker.
  • Select habits.json from the file sidebar.

You will see a JSON array containing the habit's name field. You will also see its completed value set to false.

Save file missing?

Check that save_habits(habits) appears directly after the list appends the new dictionary. Confirm that DATA_FILE contains habits.json.

Add the habit again after saving the latest version of habit_tracker.py. Help me find why habits.json is not being created.

Restore the list at startup

The save file now contains the habit. The final connection replaces the fresh empty list with the value returned by load_habits().

  • Select habit_tracker.py from the file sidebar.
  • Replace habits = [] inside main() with this line:
habits = load_habits()

Why load inside main?

The assignment runs once when the tracker starts. Every menu action then works with the list reconstructed from habits.json.

✔️ Awesome, I've got everything!

Great. Your loader now replaces the empty startup list.

ⓧ I'd like to double check the full code

import json

DATA_FILE = "habits.json"


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

    return []


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


def list_habits(habits):
    if not habits:
        print("\nNo habits yet.")
        return

    print("\nYour habits:")
    for habit in habits:
        print(habit["name"])


def add_habit(habits):
    name = input("Habit name: ").strip()
    if not name:
        print("Habit name cannot be empty.")
        return

    habits.append({"name": name, "completed": False})
    save_habits(habits)
    print(f"Added: {name}")


def main():
    habits = load_habits()

    while True:
        print("\nHabit Tracker")
        print("1. List habits")
        print("2. Add habit")
        print("3. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            list_habits(habits)
        elif choice == "2":
            add_habit(habits)
        elif choice == "3":
            print("See you next time.")
            break
        else:
            print("Choose a number from 1 to 3.")


if __name__ == "__main__":
    main()

Before you run it again, do you expect the new process to recover the saved habit? Make your prediction now.

  • Save habit_tracker.py.
  • Click the Run Python File play button.
  • Enter 1 at the menu prompt.

You will see Walk for 20 minutes in the habit list. You will also see habits.json beside habit_tracker.py in the file sidebar.

  • Enter 3 to quit the tracker.

That restart closes the persistence gap because your tracker now rebuilds its habit list from disk.

Habit disappearing again?

Check that main() assigns the result of load_habits() to habits. Confirm that habits.json remains beside habit_tracker.py.

Make sure the habit appears in habits.json before restarting the tracker. Help me diagnose why my saved habit disappears after a restart.

Your habits now survive fresh program runs. Next, you will turn their saved completion fields into a safe progress action.

Complete Habits Safely

Your tracker already carries saved habits across restarts through JSON. That persistence gives each completion a lasting effect.

A saved name cannot show progress by itself. The tracker needs numbered status labels plus a guarded selection path that stops invalid input before it changes data.

In this step, get ready to:
  • Show a number plus a completion status beside every habit.
  • Complete a habit by selecting its number.
  • Reject nonnumeric plus out-of-range selections without crashing.
Number habits and show their status

The Python function enumerate() pairs each habit with a number. Starting at 1 gives people the same numbering they see in the menu.

  • Switch back to habit_tracker.py from the previous step.
  • Select the existing list_habits(habits) function from its def line through print(habit["name"]).
  • Replace the selected function by pasting this code:
def list_habits(habits):
    if not habits:
        print("\nNo habits yet.")
        return

    print("\nYour habits:")
    for index, habit in enumerate(habits, start=1):
        status = "Done" if habit["completed"] else "Not done"
        print(f"{index}. {habit['name']} [{status}]")

What Does This Function Do?

  • The empty-list check keeps the existing friendly message when no habits are saved.
  • The enumerate(habits, start=1) loop gives each habit a visible number beginning at 1.
  • The conditional expression converts the completed Boolean into either Done or Not done.
  • The final line prints the number plus the habit name plus its current status.
  • Save habit_tracker.py.
  • Click the Run Python File play button in the editor toolbar.
  • Enter 1 at the menu prompt.

You should see every saved habit with a number plus [Not done]. Good progress. Your tracker now displays the status information needed for completion.

Missing the Number or Status?

Check that the loop includes start=1. Confirm that each saved habit in habits.json has a completed field.

Make sure the final print line remains inside the loop. Help me debug my numbered habit list.

Validate and save a completion

Every menu response begins as text. Input validation converts that text safely before using it as a list position.

A nonnumeric response causes a ValueError exception. A separate range check stops valid numbers that do not point to a habit.

  • Find the end of add_habit(habits) in habit_tracker.py.
  • Place your cursor on the blank line above def main():.
  • Add the completion function by pasting this code:
def complete_habit(habits):
    if not habits:
        print("Add a habit before completing one.")
        return

    list_habits(habits)
    selection = input("Habit number to complete: ").strip()

    try:
        index = int(selection) - 1
    except ValueError:
        print("Enter a whole number.")
        return

    if not 0 <= index < len(habits):
        print("That habit number does not exist.")
        return

    habits[index]["completed"] = True
    save_habits(habits)
    print(f"Completed: {habits[index]['name']}")

How Does Safe Completion Work?

  • The first check stops completion when the habit list is empty.
  • The function shows the numbered list before asking for a selection.
  • The expression int(selection) - 1 converts a visible habit number into its zero-based list index.
  • The except ValueError branch handles text that cannot become a whole number.
  • The range check rejects indexes outside the current list.
  • A valid selection changes completed to True before saving the updated list.
  • Select the existing main() function from def main(): through the final invalid-choice message.
  • Replace the selected function with this updated menu:
def main():
    habits = load_habits()

    while True:
        print("\nHabit Tracker")
        print("1. List habits")
        print("2. Add habit")
        print("3. Complete habit")
        print("4. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            list_habits(habits)
        elif choice == "2":
            add_habit(habits)
        elif choice == "3":
            complete_habit(habits)
        elif choice == "4":
            print("See you next time.")
            break
        else:
            print("Choose a number from 1 to 4.")

What Changed in the Menu?

  • Option 3 now calls complete_habit(habits) with the loaded list.
  • The quit action moves to option 4.
  • The fallback message now matches the four available choices.
  • Save habit_tracker.py.
  • Click the Run Python File play button in the editor toolbar.

You should see 3. Complete habit plus 4. Quit in the menu. The completion function is now connected to the running tracker.

Completion Option Missing?

Check that print("3. Complete habit") sits inside the while True: loop. Confirm that the matching branch calls complete_habit(habits).

Verify that the quit branch now checks for "4". Help me fix my completion menu option.

✔️ Awesome, I've got everything!

Your completion function plus four-option menu are in place. Double-check that you saved habit_tracker.py.

ⓧ I'd like to double check the full code

import json

DATA_FILE = "habits.json"


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

    return []


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


def list_habits(habits):
    if not habits:
        print("\nNo habits yet.")
        return

    print("\nYour habits:")
    for index, habit in enumerate(habits, start=1):
        status = "Done" if habit["completed"] else "Not done"
        print(f"{index}. {habit['name']} [{status}]")


def add_habit(habits):
    name = input("Habit name: ").strip()
    if not name:
        print("Habit name cannot be empty.")
        return

    habits.append({"name": name, "completed": False})
    save_habits(habits)
    print(f"Added: {name}")


def complete_habit(habits):
    if not habits:
        print("Add a habit before completing one.")
        return

    list_habits(habits)
    selection = input("Habit number to complete: ").strip()

    try:
        index = int(selection) - 1
    except ValueError:
        print("Enter a whole number.")
        return

    if not 0 <= index < len(habits):
        print("That habit number does not exist.")
        return

    habits[index]["completed"] = True
    save_habits(habits)
    print(f"Completed: {habits[index]['name']}")


def main():
    habits = load_habits()

    while True:
        print("\nHabit Tracker")
        print("1. List habits")
        print("2. Add habit")
        print("3. Complete habit")
        print("4. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            list_habits(habits)
        elif choice == "2":
            add_habit(habits)
        elif choice == "3":
            complete_habit(habits)
        elif choice == "4":
            print("See you next time.")
            break
        else:
            print("Choose a number from 1 to 4.")


if __name__ == "__main__":
    main()
Test every selection path

A valid selection should update one habit plus save that change. Two invalid selections should return friendly messages without a traceback.

  • Enter 2 in the running tracker.
  • Enter Read 10 pages as the habit name.
  • Enter 2 at the next menu prompt.
  • Enter Walk for 20 minutes as the habit name.

Before you test completion, which inputs do you expect the tracker to accept without losing control of the menu?

  • Enter 3 at the menu prompt.
  • Enter 1 at the habit-number prompt.

You should see a message beginning with Completed: for the selected habit. The valid path has updated the dictionary plus saved the new Boolean value.

What Did the Valid Path Prove?

The displayed number was converted into a zero-based index. The tracker used that index to change exactly one habit.

Calling save_habits(habits) writes the updated completed value to habits.json.

  • Enter 3 at the menu prompt.
  • Enter hello at the habit-number prompt.
  • Enter 3 at the next menu prompt.
  • Enter 99 at the habit-number prompt.
  • Enter 1 at the next menu prompt.

The text input should produce Enter a whole number. The out-of-range input should produce That habit number does not exist. without a traceback.

Your final list should show the selected habit as [Done]. The other incomplete habits should still show [Not done].

Seeing a Traceback or the Wrong Habit?

Check that int(selection) - 1 sits inside the try block. Confirm that the range check runs before the list update.

Make sure the update uses habits[index]["completed"] = True. Help me diagnose my completion selection logic.

You can now complete a persisted habit without letting bad input crash the tracker. Next, you will add progress totals plus a daily reset plus recovery from damaged data.

Add Progress, Reset, and Recovery

Your Python tracker now marks a selected habit complete without crashing on bad input. As the list grows, individual status labels make overall progress harder to scan.

A daily tracker also needs one action that clears every completion for a fresh day.

A damaged JSON file should produce a useful warning instead of ending the program with a traceback. This step adds that recovery before you finish the base tracker.

In this step, get ready to:
  • Report completed habits against the total number of habits.
  • Reset every saved habit for a fresh day.
  • Recover safely when habits.json contains missing or invalid data.
Show completion progress

The tracker already stores each completion as a Boolean value. Python can add those values with sum() because completed habits contribute to the total.

  • In habit_tracker.py from earlier, find the end of save_habits(habits).
  • Add the progress function below save_habits(habits) by pasting this code:
def show_progress(habits):
    completed_count = sum(habit["completed"] for habit in habits)
    print(f"Progress: {completed_count}/{len(habits)} complete")

What does this function do?

  • The generator reads the completed value from every habit.
  • The sum() call counts each True value as one completed habit.
  • The formatted string displays the completed count beside the full list length.

The total belongs after the numbered list. This placement prints one summary after every habit has been displayed.

  • Find the existing list_habits(habits) function.
  • Replace that function with this updated version:
def list_habits(habits):
    if not habits:
        print("\nNo habits yet.")
        return

    print("\nYour habits:")
    for index, habit in enumerate(habits, start=1):
        status = "Done" if habit["completed"] else "Not done"
        print(f"{index}. {habit['name']} [{status}]")

    show_progress(habits)

How does the list report progress?

  • The loop still prints each saved habit with its numbered completion status.
  • The call to show_progress(habits) sits outside the loop so the summary appears once.
  • An empty tracker still returns before calculating a total.

Before you run the tracker, what completion total do you expect from the two habits you tested in the previous step?

  • Save habit_tracker.py.
  • Click the Run Python File play button in the editor toolbar.
  • Choose option 3 if neither saved habit is complete.
  • Enter the number of one habit that shows Not done.
  • Choose option 1 to list the two habits.

You will see one habit marked Done. The list ends with Progress: 1/2 complete.

Progress total missing?

Confirm that show_progress(habits) is defined above list_habits(habits). Check that the function call lines up with the for statement instead of the code inside the loop.

Make sure both function names use the same spelling. Help me fix my missing habit progress total.

Reset every habit

A reset keeps the habit names while clearing their current completion states. Saving the changed list makes the fresh status survive the next restart.

  • Find the end of complete_habit(habits) in habit_tracker.py.
  • Add the reset function below complete_habit(habits) by pasting this code:
def reset_habits(habits):
    if not habits:
        print("There are no habits to reset.")
        return

    for habit in habits:
        habit["completed"] = False

    save_habits(habits)
    print("All habits have been reset.")

What does the reset do?

  • The first condition stops early when there are no saved habits.
  • The loop changes every completed value to False.
  • The save call writes the reset values back to habits.json.

The reset function needs its own menu option. The quit action moves to option 5 so both actions remain available.

  • Find the current main() function near the bottom of habit_tracker.py.
  • Select from def main(): through the final main() call.
  • Replace the selected code with this five-option menu:
def main():
    habits = load_habits()

    while True:
        print("\nHabit Tracker")
        print("1. List habits")
        print("2. Add habit")
        print("3. Complete habit")
        print("4. Reset habits")
        print("5. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            list_habits(habits)
        elif choice == "2":
            add_habit(habits)
        elif choice == "3":
            complete_habit(habits)
        elif choice == "4":
            reset_habits(habits)
        elif choice == "5":
            print("See you next time.")
            break
        else:
            print("Choose a number from 1 to 5.")


if __name__ == "__main__":
    main()

How does the new menu work?

  • Option 4 passes the loaded list to reset_habits(habits).
  • Option 5 ends the loop after printing the goodbye message.
  • The final branch now asks for a number from 1 to 5 when the choice is invalid.

Before you test the reset, what should happen to the progress total after every completion changes to False?

  • Save habit_tracker.py.
  • Click the Run Python File play button.
  • Choose option 4 to reset the saved habits.
  • Choose option 1 to list the reset habits.

You will see All habits have been reset. Both habits show Not done. The summary reads Progress: 0/2 complete.

Reset option not working?

Confirm that reset_habits(habits) is defined above main(). Check that option 4 calls the reset function.

Make sure save_habits(habits) remains outside the reset loop. Help me troubleshoot my habit reset action.

Recover from damaged data

The existing loader already handles a missing file. It now needs separate responses for malformed JSON and a valid JSON value with the wrong top-level shape.

  • Find the existing load_habits() function near the top of habit_tracker.py.
  • Replace that function with this recovery-aware version:
def load_habits():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            habits = json.load(file)
            if isinstance(habits, list):
                return habits
    except FileNotFoundError:
        return []
    except json.JSONDecodeError:
        print("The data file is invalid. Starting with an empty tracker.")
        return []

    print("The data file has the wrong shape. Starting with an empty tracker.")
    return []

How does recovery work?

  • The FileNotFoundError branch still starts with an empty list when the file is missing.
  • The json.JSONDecodeError branch catches malformed JSON before a traceback reaches the terminal.
  • The isinstance(habits, list) check accepts only a top-level list.
  • The final warning handles valid JSON with the wrong top-level shape.

You will temporarily damage the saved file to test the recovery branch. The valid contents stay on your clipboard so you can restore them immediately afterward.

  • Save habit_tracker.py.
  • Switch to habits.json from earlier.
  • Press Cmd+A (macOS) or Ctrl+A (Windows) to select the valid JSON.
  • Press Cmd+C (macOS) or Ctrl+C (Windows) to copy it.
  • Replace the selected JSON with this deliberately malformed value:
{

Why is this value malformed?

The opening brace starts a JSON object without completing it. Loading this file triggers the recovery path for invalid data.

  • Save habits.json.

Before you run the tracker, do you expect the damaged save file to end the program or return an empty tracker?

  • Switch back to habit_tracker.py.
  • Click the Run Python File play button.

You will see The data file is invalid. Starting with an empty tracker. The menu still runs without a traceback.

Tracker still crashes on invalid data?

Confirm that the malformed opening brace is saved in habits.json. Check that except json.JSONDecodeError: lines up with except FileNotFoundError:.

Make sure the exception name uses the same capitalization as the updated function. Help me fix invalid JSON recovery.

The recovery check passed. Restore the copied data so the project finishes with readable habit records.

  • Switch back to habits.json.
  • Press Cmd+A (macOS) or Ctrl+A (Windows) to select the malformed value.
  • Press Cmd+V (macOS) or Ctrl+V (Windows) to restore the valid JSON.
  • Save habits.json.

Before the final run, do you expect the restored habits to keep their reset completion states?

  • Switch back to habit_tracker.py.
  • Click the Run Python File play button.
  • Choose option 1 to list the restored habits.

You will see both saved habits marked Not done. The final line reads Progress: 0/2 complete.

✔️ Awesome, I've got everything!

Great work. Double-check that you saved both habit_tracker.py and the restored habits.json file.

ⓧ I'd like to double check the full code

import json

DATA_FILE = "habits.json"


def load_habits():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            habits = json.load(file)
            if isinstance(habits, list):
                return habits
    except FileNotFoundError:
        return []
    except json.JSONDecodeError:
        print("The data file is invalid. Starting with an empty tracker.")
        return []

    print("The data file has the wrong shape. Starting with an empty tracker.")
    return []


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


def show_progress(habits):
    completed_count = sum(habit["completed"] for habit in habits)
    print(f"Progress: {completed_count}/{len(habits)} complete")


def list_habits(habits):
    if not habits:
        print("\nNo habits yet.")
        return

    print("\nYour habits:")
    for index, habit in enumerate(habits, start=1):
        status = "Done" if habit["completed"] else "Not done"
        print(f"{index}. {habit['name']} [{status}]")

    show_progress(habits)


def add_habit(habits):
    name = input("Habit name: ").strip()
    if not name:
        print("Habit name cannot be empty.")
        return

    habits.append({"name": name, "completed": False})
    save_habits(habits)
    print(f"Added: {name}")


def complete_habit(habits):
    if not habits:
        print("Add a habit before completing one.")
        return

    list_habits(habits)
    selection = input("Habit number to complete: ").strip()

    try:
        index = int(selection) - 1
    except ValueError:
        print("Enter a whole number.")
        return

    if not 0 <= index < len(habits):
        print("That habit number does not exist.")
        return

    habits[index]["completed"] = True
    save_habits(habits)
    print(f"Completed: {habits[index]['name']}")


def reset_habits(habits):
    if not habits:
        print("There are no habits to reset.")
        return

    for habit in habits:
        habit["completed"] = False

    save_habits(habits)
    print("All habits have been reset.")


def main():
    habits = load_habits()

    while True:
        print("\nHabit Tracker")
        print("1. List habits")
        print("2. Add habit")
        print("3. Complete habit")
        print("4. Reset habits")
        print("5. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            list_habits(habits)
        elif choice == "2":
            add_habit(habits)
        elif choice == "3":
            complete_habit(habits)
        elif choice == "4":
            reset_habits(habits)
        elif choice == "5":
            print("See you next time.")
            break
        else:
            print("Choose a number from 1 to 5.")


if __name__ == "__main__":
    main()

That is the full tracker working. Progress is now visible at a glance.

Daily resets are saved. Damaged data now produces a friendly recovery message while valid records remain readable.

Secret mission

Turn Completions into Streaks

Replace each Boolean completion flag with a saved completion date plus a streak count. Your tracker will recognize consecutive days while preventing duplicate same-day completions.

Clean Up Your Resources

Clean Up Your Resources

Your tracker runs entirely on your Mac, so keeping it creates no ongoing charges. Choose whether to keep the project available, pause your work, or delete the local files.

Resources you used:

  • A local python-habit-tracker project folder.
  • The habit_tracker.py source file inside that folder.
  • The readable habits.json data file inside that folder.

Keep everything running

No action is needed while you are actively using the tracker. Your local files remain available without creating ongoing charges.

  • Leave the python-habit-tracker folder in place.
  • Continue recording daily completions with the tracker.
  • Keep habits.json so your habit dates and streaks remain available.

Pause - I'll come back to this later

Pausing closes your development workspace while preserving the tracker for your return. Your local files continue to create no ongoing charges.

  • Close Visual Studio Code.

Your source code stays in the python-habit-tracker folder. Your saved habits and streaks remain in habits.json.

Delete - I don't want to use this again

Complete removal deletes the project folder from your Mac. Your reusable development tools remain installed for future projects.

Deletion is permanent

Removing the folder deletes your source code plus every saved habit and streak. This is the only irreversible cleanup option.

  • Move from the open python-habit-tracker folder to its parent folder by running this command:
cd ..

Why Move Up One Folder?

The terminal starts inside the project folder. Moving to its parent gives the deletion command a stable location outside the folder being removed.

  • Delete the python-habit-tracker folder by running this command:
rm -rf python-habit-tracker

What Does This Command Remove?

This command removes the entire local project folder. The deletion includes habit_tracker.py plus habits.json.

  • Confirm the project folder is gone by running this command:
ls

What Does This Check Show?

This command lists the contents of the parent folder. A successful cleanup leaves python-habit-tracker out of that list.

You should no longer see python-habit-tracker in the output. Your project code and saved streak data are now removed.

Still See the Project Folder?

  • Check that your terminal is in the folder that previously contained python-habit-tracker.
  • Get help with the cleanup command.
  • Keep Python 3.15.2 installed for future projects.
  • Keep the Microsoft Python extension 2026.10.0 installed in Visual Studio Code.

Nice Work!

Nice Work!

You made it! You built a menu-driven Python habit tracker that preserves progress in a local JSON file. Your tracker now converts daily completions into consecutive-day streaks.

You've learned how to:

  • Build a menu-driven tracker for adding named habits. List their current status. Complete a selected habit.
  • Save habit records as readable JSON data. Reload the records after a fresh program start.
  • Use input validation to reject invalid selections. Use exception handling to recover from missing or malformed save data.
  • Secret Mission: Migrate existing habits into date-aware records. Calculate consecutive-day streaks. Prevent duplicate completion on the same day.

Ready to quiz yourself?