Summarizing Your Data

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 days

Two 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 -- consecutive

A 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 0 would mean the same day. However, as distinct_days has 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 longest

Walking 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, so current and longest start at 1.
  • 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 exactly 1, the streak extends; otherwise it has broken, and a new streak of 1 starts at the current day.
  • After updating current, the function checks whether this is a new longest run.
  • At the end, longest holds 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 0 for an empty input list,
  • return 1 when there is at least one number but no consecutive pair,
  • treat a difference of 1 as 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 4
longest_consecutive_run([2, 1, 5, 8]) # returns 2
longest_consecutive_run([]) # returns 0

Streaks and Edge Cases

Before reading on, take a minute and predict what longest_streak returns for each of these inputs:

InputPredicted 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 by distinct_days).

The next three are where having a function that checks consecutive days is important:

  • Two on consecutive days: gap is 1, current becomes 2, answer is 2.
  • Two with one day of gap: gap is 2, current resets to 1, answer is 1.
  • 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([]) == 0
assert longest_streak(make_sessions(["2026-05-10"])) == 1
assert longest_streak(make_sessions(["2026-05-10", "2026-05-10"])) == 1
assert longest_streak(make_sessions(["2026-05-10", "2026-05-11"])) == 2
assert longest_streak(make_sessions(["2026-05-10", "2026-05-12"])) == 1
assert longest_streak(make_sessions(["2026-05-10", "2026-05-11", "2026-05-12"])) == 3
assert 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"])) == 2
print("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 streak

Two 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")
# Empty
assert current_streak([], today) == 0
# One session today
assert current_streak(make_sessions(["2026-05-12"]), today) == 1
# Two consecutive days ending today
assert current_streak(make_sessions(["2026-05-11", "2026-05-12"]), today) == 2
# Latest is yesterday: streak alive
assert current_streak(make_sessions(["2026-05-11"]), today) == 1
# Latest is two days ago: streak broken
assert current_streak(make_sessions(["2026-05-10"]), today) == 0
# Three consecutive ending today
assert current_streak(
make_sessions(["2026-05-10", "2026-05-11", "2026-05-12"]),
today,
) == 3
# Order does not matter
assert current_streak(
make_sessions(["2026-05-12", "2026-05-10", "2026-05-11"]),
today,
) == 3
# Multiple sessions on the same day count as one day
assert 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:
pass
print("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

  1. Why does the first step of every streak function reduce sessions to distinct days?
  2. What is the consecutive-day test, and what type are the two values it compares?
  3. In longest_streak, why does current reset to 1 (not 0) when the streak breaks?
  4. Why does current_streak accept a today=None parameter instead of always using date.today()?
  5. 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.

Diagrams tried: 0