Streaks of Consecutive Days
Learning Objectives
- You can reduce a list of sessions to a sorted list of distinct days.
- You can compute the longest streak of consecutive days.
- You can compute the current streak.
In this chapter, we look into streaks. A streak is a run of consecutive days on which you logged at least one study session. Determining streaks pulls the tools from the previous three chapters together — dates as real values, sorting, and a single-pass loop.
Reducing Sessions to Days
Before you can talk about consecutive days, you have to deal with the fact that the data is a list of sessions. The first step of every streak computation is the same: reduce the sessions to a list of distinct days on which any session happened.
We use the find unique-values pattern from Summaries by Category, applied to the date field. The helper function uses ordinary loops throughout, including the conversion from strings to date objects:
from datetime import date
def distinct_days(sessions): """Return the distinct session dates as date objects, sorted ascending.
Returns [] for empty input. """ day_strings = [] for subject, minutes, day_string in sessions: if day_string not in day_strings: day_strings.append(day_string)
days = [] for day_string in day_strings: days.append(date.fromisoformat(day_string))
days.sort() return daysTwo passes. The first walks the sessions and collects unique date strings. The second walks the unique strings and parses each one to a date. The final sort puts the dates in ascending order.
A test:
sessions = [ ["math", 45, "2026-05-10"], ["physics", 90, "2026-05-10"], # same day -- one distinct day ["history", 30, "2026-05-11"], ["math", 60, "2026-05-12"],]
assert distinct_days(sessions) == [ date.fromisoformat("2026-05-10"), date.fromisoformat("2026-05-11"), date.fromisoformat("2026-05-12"),]assert distinct_days([]) == []What Makes Two Days Consecutive?
Once you have a sorted list of distinct days, you need a way to check whether two dates are one day apart. We use the date-subtraction from Working With Dates and Date Windows:
d1 = date.fromisoformat("2026-05-10")d2 = date.fromisoformat("2026-05-11")
(d2 - d1).days # 1 -- consecutiveA gap of 1 means consecutive. A gap of 2 or more is a break — the streak ends, and a new one might start.
A gap of
0would mean the same day. However, asdistinct_dayshas already removed duplicates, this should not happen.
Two days are consecutive if and only if (d2 - d1).days == 1.
The Longest Streak
With the data reduced and the consecutive-day test in hand, the longest-streak function is a single-pass loop that tracks two numbers: the length of the run currently being built, and the longest run seen so far.
The shape is familiar: walk the data, update a running “best” as you go.
def longest_streak(sessions): """Return the longest streak of consecutive days, in days.
Returns 0 for empty input. """ days = distinct_days(sessions) if len(days) == 0: return 0
current = 1 longest = 1 for index in range(1, len(days)): if (days[index] - days[index - 1]).days == 1: current += 1 else: current = 1
if current > longest: longest = current return longestWalking through it:
- If there are no days at all, return
0. A streak of zero is the only sensible answer for an empty log. - Otherwise, every day in the list is at least its own streak of
1, socurrentandlongeststart at1. - The loop visits each day starting from the second one (index
1). For each, it compares the day to the previous one. If the gap is exactly1, the streak extends; otherwise it has broken, and a new streak of1starts at the current day. - After updating
current, the function checks whether this is a new longest run. - At the end,
longestholds the answer.
The loop visits each day exactly once. There is no nested loop, no extra pass — this is one of the cleanest single-pass summaries in the course.
The key helper here is
range(1, len(days)), which gives you the indices of the second through last days. This allows you to compare each day to the previous one without needing to special-case the first day.
Longest consecutive number run
0 / 15 points
Write a function named longest_consecutive_run(numbers).
The function receives a list of integers. It must return the length of the longest run where each next number is exactly one larger than the previous number.
Start by sorting the numbers. Then build a new list where each value appears only once. Finally, scan that cleaned list to find the longest consecutive run.
Requirements:
- count duplicate numbers only once,
- work when the input list is unsorted,
- work with negative numbers,
- return
0for an empty input list, - return
1when there is at least one number but no consecutive pair, - treat a difference of
1as consecutive and any larger difference as a break, - do not change the original list,
- do not print anything,
- and do not use
input(...).
Examples:
longest_consecutive_run([1, 2, 3, 3, 4]) # returns 4longest_consecutive_run([2, 1, 5, 8]) # returns 2longest_consecutive_run([]) # returns 0Streaks and Edge Cases
Before reading on, take a minute and predict what longest_streak returns for each of these inputs:
| Input | Predicted answer |
|---|---|
| No sessions at all | ? |
| One session | ? |
| Two sessions on the same day | ? |
| Two sessions on consecutive days | ? |
| Two sessions with one day of gap | ? |
| Three sessions: day 1, day 3, day 4 | ? |
| Same three sessions, but given in a different order | ? |
The first three are straightforward and can be reasoned about without tracing the code:
- Empty:
0(handled by the early return). - One session:
1(it is its own streak of length one). - Two sessions on the same day:
1(the duplicate is removed bydistinct_days).
The next three are where having a function that checks consecutive days is important:
- Two on consecutive days: gap is
1,currentbecomes2, answer is2. - Two with one day of gap: gap is
2,currentresets to1, answer is1. - Three sessions on day 1, day 3, day 4: day 3 → reset to
1, day 4 → consecutive with day 3,current = 2. Answer:2.
The last one is the question that makes the function robust. Does the order of the input matter? It does not — distinct_days sorts the dates before returning them, so the order of the input is irrelevant to the answer.
These should be your tests. The helper here uses an ordinary loop (not a comprehension) because comprehensions have not been introduced yet:
def make_sessions(date_strings): """Build sessions list from a list of date strings.""" result = [] for day in date_strings: result.append(["math", 30, day]) return result
assert longest_streak([]) == 0assert longest_streak(make_sessions(["2026-05-10"])) == 1assert longest_streak(make_sessions(["2026-05-10", "2026-05-10"])) == 1assert longest_streak(make_sessions(["2026-05-10", "2026-05-11"])) == 2assert longest_streak(make_sessions(["2026-05-10", "2026-05-12"])) == 1assert longest_streak(make_sessions(["2026-05-10", "2026-05-11", "2026-05-12"])) == 3assert longest_streak(make_sessions(["2026-05-10", "2026-05-12", "2026-05-13"])) == 2
# Order does not matter:assert longest_streak(make_sessions(["2026-05-13", "2026-05-10", "2026-05-12"])) == 2print("longest_streak tests passed.")The Current Streak
The current streak is the run of consecutive days ending on a reference date — today, usually. For this, we can use the following set of rules:
- If the latest distinct day in the log is today, the streak runs back from today.
- If the latest distinct day is yesterday (one day before today), the streak runs back from yesterday. Today does not have a session yet, but the streak is still alive.
- If the latest distinct day is two days ago or earlier, the current streak is
0. The streak has broken. - If the latest distinct day is after today — a future date — something is wrong. The function raises rather than silently producing a misleading answer.
These four cases need to be checked. All of them are easy to get wrong, and three of them give the right answer on the happy-path test data.
def current_streak(sessions, today=None): """Return the current streak in days, ending at `today` (defaults to date.today()).
The streak is alive if the latest session day is today or yesterday. The streak is 0 if the latest session day is two or more days before today. Raises ValueError if any session day is in the future relative to today. Returns 0 for empty input. """ if today is None: today = date.today()
days = distinct_days(sessions) if len(days) == 0: return 0
# Defend against future dates explicitly. if days[-1] > today: raise ValueError( f"Session date {days[-1]} is in the future relative to today {today}." )
latest = days[-1] gap = (today - latest).days if gap > 1: return 0 # latest session is two or more days ago -- streak is broken
# Walk back from `latest`, counting consecutive days. streak = 1 for index in range(len(days) - 2, -1, -1): if (days[index + 1] - days[index]).days == 1: streak += 1 else: break return streakTwo things worth pointing out.
The today=None default lets callers pass a specific date, or omit it for the real “today.” Functions that have an implicit time dependency should expose that dependency explicitly — for example, so that tests can control it.
The loop walks backwards from the latest day. range(len(days) - 2, -1, -1) produces the indices from the second-to-last down to zero. This is a range with three arguments — start, stop, step. With a negative step the range counts down, and -1 as the stop means “down to zero, inclusive.”
Tests against the four cases:
today = date.fromisoformat("2026-05-12")
# Emptyassert current_streak([], today) == 0
# One session todayassert current_streak(make_sessions(["2026-05-12"]), today) == 1
# Two consecutive days ending todayassert current_streak(make_sessions(["2026-05-11", "2026-05-12"]), today) == 2
# Latest is yesterday: streak aliveassert current_streak(make_sessions(["2026-05-11"]), today) == 1
# Latest is two days ago: streak brokenassert current_streak(make_sessions(["2026-05-10"]), today) == 0
# Three consecutive ending todayassert current_streak( make_sessions(["2026-05-10", "2026-05-11", "2026-05-12"]), today,) == 3
# Order does not matterassert current_streak( make_sessions(["2026-05-12", "2026-05-10", "2026-05-11"]), today,) == 3
# Multiple sessions on the same day count as one dayassert current_streak( make_sessions(["2026-05-12", "2026-05-12", "2026-05-11"]), today,) == 2
print("current_streak tests passed.")And a separate test for the future-date case:
try: current_streak(make_sessions(["2026-05-15"]), today) raise AssertionError("Expected ValueError for future date")except ValueError: passprint("future-date guard works.")The yesterday-still-alive rule is the case worth spending a bit of time on. A session on 2026-05-11 and today being 2026-05-12 returns 1 — not 0. The reader has not logged today yet, but the streak has not broken until tomorrow. Whether this is the right rule is a design decision; some apps reset at midnight, others have a grace period. This rule was chosen because it matches how people think about a study streak: missing today does not feel like a break until the next day.
The next sequence starts from a small reading-log CSV file. First you will load and clean malformed external data, rehearsing what you learned in Defensive Programming with Files and External Data. Then you will use the cleaned rows to compute distinct days and streaks.
Check Your Understanding
- Why does the first step of every streak function reduce sessions to distinct days?
- What is the consecutive-day test, and what type are the two values it compares?
- In
longest_streak, why doescurrentreset to1(not0) when the streak breaks? - Why does
current_streakaccept atoday=Noneparameter instead of always usingdate.today()? - What does
range(len(days) - 2, -1, -1)do? Why is it the right range for walking backwards from the second-to-last day to the first day?
AI Study Aid
Create a chapter diagram
We're looking into to what extent AI-generated diagrams could help with studying.
Use this study aid to generate an AI-generated visual summary of the material you just studied.
Each diagram style emphasizes the content in a different way, so you can choose the focus
that feels most useful.
Using the diagram generator is voluntary and does not affect your course progress. We encourage
you to try it out and see if it helps, but it's totally up to you! Your answers help us understand
how to make better study aids in the future.