Skip to Content
Course content

423: Building a Habit Tracker Application

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

I want to start this lesson with a piece of code that looks perfectly fine at first glance. I actually wrote something like this in a project a few years ago, and it drove me crazy for an hour because the behavior seemed completely random. Here is a simplified version of a HabitTracker class where we're trying to let users keep track of their daily wins.

class HabitTracker:
    def __init__(self, username, habits=[]):
        self.username = username
        self.habits = habits

    def add_habit(self, habit):
        self.habits.append(habit)

# Create two different users
alice = HabitTracker("Alice")
bob = HabitTracker("Bob")

alice.add_habit("Drink Water")
print(f"Alice's habits: {alice.habits}") # Expected: ['Drink Water']
print(f"Bob's habits: {bob.habits}")     # Expected: []

If you run this, you'll notice something weird: Bob has "Drink Water" in his list, even though we only added it to Alice. It feels like a ghost is haunting the code. What's happening here is one of the most notorious traps in Python: the mutable default argument.

The Ghost in the Default List

In Python, default arguments are evaluated only once—at the time the function (or method) is defined, not every time it's called. Because a list [] is a mutable object, Python creates one single list in memory and attaches it to the __init__ method. Every time you create a new HabitTracker without providing a specific list of habits, Python points that new instance to the exact same list object.

So, when Alice adds a habit, she's modifying the shared default list. When Bob is created, he's handed that same modified list. It's a classic "gotcha" that can lead to some nightmare-inducing bugs in larger applications.

Stopping the Leak with None

The standard way to handle this—and how I do it now—is to use None as the default value. This tells Python, "I don't have a value yet," and then we assign a fresh, empty list inside the method body. Since the assignment happens during the call, a new list is created for every single instance.

class HabitTracker:
    def __init__(self, username, habits=None):
        self.username = username
        if habits is None:
            self.habits = []
        else:
            self.habits = habits

    def add_habit(self, habit):
        self.habits.append(habit)

Now, Alice and Bob each get their own private list, and the "ghost" is gone.

Designing the Habit Logic

Now that we've cleared that up, let's think about how a real habit tracker actually works. A simple list of strings isn't enough; we need to know when the habit was completed. I find that using a dictionary is the most efficient way to handle this. The key is the name of the habit, and the value is a list of dates.

We'll want to use the datetime module to handle dates. It's much cleaner than trying to manage date strings manually. Here is how I would structure the core logic for adding a completion mark:

from datetime import date

class HabitTracker:
    def __init__(self, username):
        self.username = username
        self.habits = {} # Format: {"Reading": [date(2023, 10, 1), date(2023, 10, 2)]}

    def add_habit(self, habit_name):
        if habit_name not in self.habits:
            self.habits[habit_name] = []

    def complete_habit(self, habit_name):
        today = date.today()
        if habit_name in self.habits:
            if today not in self.habits[habit_name]:
                self.habits[habit_name].append(today)

Notice how I checked if today not in self.habits[habit_name]. You don't want a user to "complete" the same habit five times in one day and accidentally inflate their stats. It's a small detail, but these are the things that make an app feel professional.

Calculating the Streak

The most satisfying part of any habit tracker is the streak. To calculate this, we need to look at the list of dates for a habit, sort them, and count backwards from today to see how many consecutive days the habit was completed.

I usually recommend converting the date list into a set for faster lookups. When you're checking "Did they do this on Tuesday?" and "Did they do this on Wednesday?", a set is significantly more performant than scanning a list over and over again.




📋 Practical Task

Implementing the Current Streak Counter

You have been provided with a basic HabitTracker class. Your task is to implement a method called get_streak(self, habit_name). This method should return the number of consecutive days (including today) that the habit has been completed.

Requirements:

  • If the habit hasn't been completed today or yesterday, the streak is 0.
  • If it was completed today, start counting backwards from today.
  • If it wasn't completed today but was completed yesterday, the streak is still active (counting from yesterday).
  • Use datetime.date.today() and datetime.timedelta(days=n) to calculate previous dates.
from datetime import date, timedelta

class HabitTracker:
    def __init__(self, username):
        self.username = username
        self.habits = {}

    def add_habit(self, habit_name):
        if habit_name not in self.habits:
            self.habits[habit_name] = []

    def complete_habit(self, habit_name, day=None):
        if day is None:
            day = date.today()
        if habit_name in self.habits and day not in self.habits[habit_name]:
            self.habits[habit_name].append(day)

    def get_streak(self, habit_name):
        # YOUR CODE HERE
        pass

# Test your code
tracker = HabitTracker("DevUser")
tracker.add_habit("Coding")
today = date.today()
tracker.complete_habit("Coding", today)
tracker.complete_habit("Coding", today - timedelta(days=1))
tracker.complete_habit("Coding", today - timedelta(days=2))
# Streak should be 3
print(f"Current streak: {tracker.get_streak('Coding')}") 
Rating
0 0

There are no comments for now.

to be the first to leave a comment.