LESSON 137 · DEEPER PYTHON30 min

Build a Python Habit Streak Analyzer

Find the longest run of consecutive calendar dates in a list of habit check-ins.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Build a Python Habit Streak Analyzer

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Suppose a habit app exports these three dates: March first, February twenty- seventh, and February twenty-eighth. The events arrived out of order, but the actual calendar contains a three-day streak. Pause and predict what a simple loop over the file will print. Do not confuse the number of check-ins with the number of distinct days. We will make one small function with a precise contract: return the longest historical sequence of consecutive dates. Later, a product might also report the active streak ending today, but that is a different calculation that depends on a timezone and the definition of today. For this project, the list of date strings is all the input we need.

  2. 00:43

    A habit log is a set of calendar days, not a sequence of button presses. A person can submit the same date twice and can import older entries after newer ones. To find a streak, parse each ISO date with date.fromisoformat, remove duplicates, sort the resulting date objects, and compare adjacent days with timedelta(days=1). The longest streak is the largest consecutive run anywhere in the history; it is different from a current streak ending today. This offline project needs no account, clock, or network call. Watch the first terminal run. The function parses each ISO string correctly but walks the list in the order

  3. 01:23

    it was supplied. It sees March first followed by February twenty-seventh, which is a backwards jump. It resets the run. Then it sees February twenty-eighth, which is only a two-day run from the previous item. The reported best length is wrong for the actual calendar history. This is a useful example because the loop body looks plausible and works on neatly ordered hand-written data. The error is in an assumption about input order. Before changing the condition, we need to inspect the representation of the input and normalize it. The first implementation walks input order.

  4. 02:03

    It reports a two-day streak for March first, February twenty-seventh, and February twenty-eighth because those entries are out of order. A string comparison can also hide assumptions about formatting. Print the parsed dates and their sorted order before changing the algorithm. Duplicates are another trap: two records for one date must not add a day to the streak or interrupt a valid run. Decide what the function means before trying to optimize it. The diagnosis prints both the raw strings and sorted date objects. It also includes February twenty-eighth twice. We can now see two independent facts: the file order is not chronological, and a

  5. 02:43

    repeated check-in is not a new day. We convert first, because date objects make calendar operations explicit. We put them in a set to remove repeated dates, then sort the set so each comparison goes forward through time. Notice that a set itself has no useful chronological order; sorting is a separate step. This normalized list is the foundation for a reliable streak counter. Predict what the consecutive differences should be for the three remaining dates before the fixed run begins. After normalization, keep a current run length and a best run length. The first date starts a run of one.

  6. 03:24

    If the next date minus the previous date equals one calendar day, increase the current run; otherwise start a new run at one. Update the best length on every iteration. Python's date arithmetic handles month boundaries and leap days, so do not add one to a day number by hand. Parsing invalid date strings should raise ValueError instead of silently inventing a date. The corrected function starts with zero for both best and current run, which gives the empty input a defined result. Each parsed day either extends the previous day by exactly one timedelta day or starts a new run of one. We update best after every date, not only when a run ends, so the final run cannot

  7. 04:06

    be forgotten. The output is three for the sample. Pay attention to the kind of value being subtracted: date minus date yields a timedelta, which can cross February to March or December to January safely. The program has no hidden dependency on the current clock, so it remains reproducible in the browser exercise. Read the fixed code from top to bottom. The set comprehension calls date.fromisoformat once for each supplied value, which validates the format while preparing values for arithmetic. Sorting produces a stable sequence. In the loop, index zero cannot have a previous day, so it starts a run.

  8. 04:49

    For every later day, the expression compares it with the immediately previous normalized day. If their difference equals one timedelta day, the run grows; otherwise it resets. best records the largest value seen so far. This design is linear after sorting, apart from the sort itself, and uses storage for distinct dates. Its output can be tested exactly without waiting for a real habit tracker to collect data. The final run uses a leap year and a duplicate February twenty-ninth. February twenty-eighth, February twenty- ninth, and March first form three different calendar days, so the answer is three.

  9. 05:29

    The repeated February twenty-ninth contributes no extra day. An empty log returns zero. Try another variation yourself: include a gap after March first and a longer run later in the history. Check that the function returns the maximum, even when it does not end on the latest date. If a date string is malformed, date.fromisoformat should raise an error; do not silently treat it as a valid check- in. The article includes the task checker and a short quiz to test those boundaries. An empty log has longest streak zero. A leap-year sequence from February twenty- eighth through March first has three

  10. 06:09

    calendar dates when February twenty-ninth is present. The verification run checks both this boundary and a duplicated middle date. Because this project computes the historical maximum, adding a missed old date may change the result even if the latest date does not. A future product could compute an active streak separately using a chosen timezone and today; this tutorial intentionally keeps that policy out of its simple function.

Understand the concept

A habit log is a set of calendar days, not a sequence of button presses. A person can submit the same date twice and can import older entries after newer ones. To find a streak, parse each ISO date with date.fromisoformat, remove duplicates, sort the resulting date objects, and compare adjacent days with timedelta(days=1). The longest streak is the largest consecutive run anywhere in the history; it is different from a current streak ending today. This offline project needs no account, clock, or network call.

The first implementation walks input order. It reports a two-day streak for March first, February twenty-seventh, and February twenty-eighth because those entries are out of order. A string comparison can also hide assumptions about formatting. Print the parsed dates and their sorted order before changing the algorithm. Duplicates are another trap: two records for one date must not add a day to the streak or interrupt a valid run. Decide what the function means before trying to optimize it.

After normalization, keep a current run length and a best run length. The first date starts a run of one. If the next date minus the previous date equals one calendar day, increase the current run; otherwise start a new run at one. Update the best length on every iteration. Python's date arithmetic handles month boundaries and leap days, so do not add one to a day number by hand. Parsing invalid date strings should raise ValueError instead of silently inventing a date.

An empty log has longest streak zero. A leap-year sequence from February twenty-eighth through March first has three calendar dates when February twenty-ninth is present. The verification run checks both this boundary and a duplicated middle date. Because this project computes the historical maximum, adding a missed old date may change the result even if the latest date does not. A future product could compute an active streak separately using a chosen timezone and today; this tutorial intentionally keeps that policy out of its simple function.

  • Parse ISO strings into date objects
  • Deduplicate before counting
  • Sort before comparing adjacent days
  • Compare dates with a one-day timedelta

See it step by step

Read the code, predict the output, then compare it with the result.

01. See chronological order

PYTHON
from datetime import date
raw = ['2026-03-01','2026-02-27','2026-02-28']
print(sorted(date.fromisoformat(value) for value in raw))
EXPECTED OUTPUT
[datetime.date(2026, 2, 27), datetime.date(2026, 2, 28), datetime.date(2026, 3, 1)]

Parsed date objects sort by actual calendar order, exposing why input order cannot be trusted.

02. Collapse repeated days

PYTHON
from datetime import date
raw = ['2024-02-28','2024-02-29','2024-02-29','2024-03-01']
print(len(sorted({date.fromisoformat(value) for value in raw})))
EXPECTED OUTPUT
3

Four check-ins contain three distinct calendar dates.

03. Cross leap day

PYTHON
from datetime import date, timedelta
a = date.fromisoformat('2024-02-29')
b = date.fromisoformat('2024-03-01')
print(b - a == timedelta(days=1))
EXPECTED OUTPUT
True

Date subtraction handles the leap-year transition without manual month logic.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Order the history

A habit log is a set of calendar days, not a sequence of button presses. A person can submit the same date twice and can import older entries after newer ones. To find a streak, parse each ISO date with date.fromisoformat, remove duplicates, sort the resulting date objects, and compare adjacent days with timedelta(days=1). The longest streak is the largest consecutive run anywhere in the history; it is different from a current streak ending today. This offline project needs no account, clock, or network call.

First predict what the raw loop does when a newer date appears before two older dates. Compare that with the chronological printout. A historical streak depends on date order, not file order or the time a record was imported. Explain why a set changes the input contract before using it.

PYTHON
from datetime import date
raw = ['2026-03-01','2026-02-27','2026-02-28']
print(sorted(date.fromisoformat(value) for value in raw))
EXPECTED OUTPUT
[datetime.date(2026, 2, 27), datetime.date(2026, 2, 28), datetime.date(2026, 3, 1)]
Follow the reasoning
  1. Parse every ISO string.
  2. Compare original order with sorted date objects.
02 / 03

Count distinct check-in days

The first implementation walks input order. It reports a one-day streak for March first, February twenty-seventh, and February twenty-eighth because those entries are out of order. A string comparison can also hide assumptions about formatting. Print the parsed dates and their sorted order before changing the algorithm. Duplicates are another trap: two records for one date must not add a day to the streak or interrupt a valid run. Decide what the function means before trying to optimize it.

A user may click twice on one day. A streak counts days, so count distinct parsed dates. Run this example with another repeated value and verify that the number remains the number of calendar days. Keep a separate event-count metric if the product later needs to count every click.

PYTHON
from datetime import date
raw = ['2024-02-28','2024-02-29','2024-02-29','2024-03-01']
print(len(sorted({date.fromisoformat(value) for value in raw})))
EXPECTED OUTPUT
3
Follow the reasoning
  1. Deduplicate parsed dates.
  2. Explain why a second check-in is not another day.
03 / 03

Test calendar boundaries

After normalization, keep a current run length and a best run length. The first date starts a run of one. If the next date minus the previous date equals one calendar day, increase the current run; otherwise start a new run at one. Update the best length on every iteration. Python's date arithmetic handles month boundaries and leap days, so do not add one to a day number by hand. Parsing invalid date strings should raise ValueError instead of silently inventing a date.

February has a variable length, and December to January crosses a year. Use timedelta comparison instead of checking integer day components. Try the 2024 leap-year boundary, the 2025 non-leap boundary, and an empty sequence after implementing the task.

PYTHON
from datetime import date, timedelta
a = date.fromisoformat('2024-02-29')
b = date.fromisoformat('2024-03-01')
print(b - a == timedelta(days=1))
EXPECTED OUTPUT
True
Follow the reasoning
  1. Subtract adjacent dates.
  2. Test leap day and the empty list.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnMake longest_streak accept dates in any order, ignore repeated check-ins, return zero for an empty log, and reject invalid ISO dates. The prepared sample should print 3.
Need a hint?

Build sorted({date.fromisoformat(value) for value in values}) before the loop; initialize best to zero and increase run only across a one-day timedelta.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Why remove duplicate check-ins before counting?

    02.What does date.fromisoformat return?

    03.Does longest streak necessarily end today?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Trusting log order

    COMMON MISTAKE
    for old, new in zip(days, days[1:])
    FIX
    days = sorted(set(days))

    What happens: An imported old entry breaks a real streak.
    Normalize chronology and collapse repeated check-ins before comparing neighbors.

    Counting day numbers

    COMMON MISTAKE
    new.day == old.day + 1
    FIX
    new - old == timedelta(days=1)

    What happens: Month and leap-year boundaries fail.
    Calendar date subtraction handles transitions between months and years.

    Browse this lesson’s problem fixes

    Sources and further reading