Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
423: Building a Habit Tracker Application
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()anddatetime.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')}")
There are no comments for now.