1 The Problem
We want a habit tracker: define habits, mark them done on a given day, and compute the current streak — how many consecutive days you have kept it up. It teaches working with dates and the satisfying-but-tricky logic of finding consecutive runs.
2 How to Think About It
Think about what a streak means, before any code:
3 The Build — explained part by part
Here is the complete habit tracker. The streak logic uses real dates. Each part is explained below.
from datetime import date, timedelta
def mark_done(completed_dates, day=None):
# Record a habit as done on a day (default today).
day = day or date.today()
completed_dates.add(day.isoformat())
return completed_dates
def current_streak(completed_dates, today=None):
today = today or date.today()
streak = 0
day = today
# Step backwards one day at a time while each is completed.
while day.isoformat() in completed_dates:
streak += 1
day = day - timedelta(days=1)
return streak
if __name__ == "__main__":
done = set()
mark_done(done, date(2026, 6, 22))
mark_done(done, date(2026, 6, 23))
mark_done(done, date(2026, 6, 24))
print("Streak:", current_streak(done, date(2026, 6, 24)))
date represents a calendar day; timedelta represents a span (like “one day”) we can subtract.completed_dates — a set of date strings (ISO format like
"2026-06-24"). A set gives instant “was this day done?” checks and ignores duplicate marks.day.isoformat() — turn a date into its text form for storing and comparing.
the while loop in current_streak — the heart of it: start at today, and while that day is in the completed set, count it and step back one day with
day - timedelta(days=1). The moment a day is missing, the streak ends.day = day - timedelta(days=1) — proper date maths: subtracting a timedelta correctly handles month and year boundaries, which manual arithmetic would get wrong.
timedelta(days=1) so month and year rollovers are handled..isoformat() for a consistent format.4 Test & Prove Each Part
We test the streak logic with fixed dates, so we can verify the consecutive-day counting exactly.
from datetime import date, timedelta
def current_streak(completed_dates, today):
streak = 0
day = today
while day.isoformat() in completed_dates:
streak += 1
day = day - timedelta(days=1)
return streak
def test_three_day_streak():
"""Three consecutive days give a streak of 3."""
dates = {"2026-06-22", "2026-06-23", "2026-06-24"}
assert current_streak(dates, date(2026, 6, 24)) == 3
def test_gap_breaks_streak():
"""A missing day stops the streak."""
dates = {"2026-06-22", "2026-06-24"} # 23rd missing
assert current_streak(dates, date(2026, 6, 24)) == 1
def test_no_completions():
"""No completions means a streak of 0."""
assert current_streak(set(), date(2026, 6, 24)) == 0
Run with pytest -v. By passing a fixed today, the tests are deterministic — they do not depend on the real date. The gap test proves a missing day correctly breaks the streak.
5 The Interface
Returns
Streak: 36 Run It & Automate It
python3 habits.pySee a 3-day streak computed. Mark habits done each day to build real streaks.
Jenkins runs the tests automatically — each line explained below.
Streak: 3.isoformat() consistently.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Longest streak. Track the best streak ever, not just current. (Teaches: scanning all dates.)
- Multiple habits. Track several habits at once. (Teaches: a dict of habits.)
- Weekly view. Show a calendar grid of done days. (Teaches: date formatting.)
date and timedelta, and to compute consecutive runs by stepping backward day by day. Proper date maths avoids the bugs that manual day-counting causes. Related: datetime, Control Flow.