Summarizing Your Data

Working With Dates and Date Windows


Learning Objectives

  • You can parse an ISO-format string into a date using date.fromisoformat, and recognise that the result is a different type from the string.
  • You can compare two dates with <, ==, >, and subtract them to get a number of days.
  • You can add or subtract timedelta values to compute “the date N days from D.”
  • You can write a function that selects sessions whose dates fall within an inclusive date window.

The study log has been storing dates as strings like "2026-05-10" since the last part, and using date.today() to record each session’s date. That is enough for recording and displaying dates, but it is not enough for asking time-shaped questions. How many minutes did I study in the last seven days? Which sessions were today? How many days apart are these two sessions? All of those require treating dates as values you can compute with — comparing them, subtracting them, and selecting ranges of them.

In this chapter, we look at the mechanics of working with dates. The next two chapters apply these mechanics to write summaries by day and streaks of consecutive days.

Strings That Look Like Dates Are Not Dates

ISO-format date strings like "2026-05-10" sort and compare correctly as text. The string "2026-05-10" is less than the string "2026-05-11" according to Python’s string comparison — and that happens to be the same answer you would get if these were real dates.

"2026-05-10" < "2026-05-11" # True
"2026-01-15" < "2026-12-31" # True

The reason for this is that the ISO format puts the most significant part of the date (the year) first, then the month, then the day. For instance, for the first line, the comparison iterates through the characters of the two strings in order, comparing them one by one, until it finds a difference.

What does not work is arithmetic. You cannot subtract two strings, and even if you could, the answer you would want (one day) is not a string. There is also no way to ask “what is the date three days before this one?” with string operations alone.

So for sorting and equality, ISO date strings work by accident. For arithmetic — the operations that let you answer questions about time — you need real date objects.

Recap of date.today()

You used date.today() in the project chapter of the last part to record the current date when saving a session:

from datetime import date
today = date.today()
print(today) # 2026-05-12
print(today.isoformat()) # '2026-05-12' -- back to a string

date.today() returns the current date as a date object. .isoformat() converts it back to a YYYY-MM-DD string. The .isoformat() form was written into the CSV, which is what makes the file column easy to read and to sort.

This chapter expands from there: parsing dates out of strings, doing arithmetic on them, and using them to select time windows.

Parsing a Date From a String

The mirror of .isoformat() is date.fromisoformat. Given a string in the YYYY-MM-DD shape, it returns a date:

from datetime import date
s = "2026-05-10"
d = date.fromisoformat(s)
print(d) # 2026-05-10
print(type(d)) # <class 'datetime.date'>

A date is a different type from a string. Date objects have attributes and support operations that strings do not.

The display of d looks identical to the display of s, which can be a small source of confusion. The function type helps clarify the difference. The string s is of type str, while the date d is of type datetime.date.

If the string is not in the expected ISO format — "11/5/2026", "May 11, 2026", "not a date" — date.fromisoformat raises ValueError:

date.fromisoformat("not a date")
# ValueError: Invalid isoformat string: 'not a date'

This is the same ValueError from the chapter on exceptions from the third part. You handle it the same way — with try/except when you anticipate that the input might be bad.

Invalid date strings

0 / 5 points

A window function is told to skip rows with invalid date strings. What should it do when date.fromisoformat(...) raises ValueError?

Comparing Dates

Once we have parsed a string into a date, we can compare it to other dates. date objects support the usual comparison operators: <, <=, >, >=, ==, !=. These compare dates as dates — earlier dates are less than later ones.

d1 = date.fromisoformat("2026-05-10")
d2 = date.fromisoformat("2026-05-11")
d1 < d2 # True
d1 == d2 # False

For ISO-format strings the answers happen to match what string comparison would give. For dates in other formats they would not. The benefit of using date objects is that the comparison is correct by construction, regardless of how the strings happened to look.

Subtracting Dates: timedelta

Subtracting one date from another gives a timedelta — an object representing the gap between them:

d1 = date.fromisoformat("2026-05-10")
d2 = date.fromisoformat("2026-05-15")
diff = d2 - d1
print(diff) # 5 days, 0:00:00
print(diff.days) # 5

The display of timedelta is verbose (“5 days, 0:00:00”), but the attribute you almost always want is .days — the integer number of days between the two dates. For everything in this course, .days is the only attribute of a timedelta you need.

The order matters. d2 - d1 is positive when d2 is later, negative when d1 is.

timedelta also works as an input — you can add or subtract a timedelta to or from a date to get a new date:

from datetime import date, timedelta
today = date.today()
tomorrow = today + timedelta(days=1)
last_week = today - timedelta(days=7)
six_days_ago = today - timedelta(days=6)

This is how you ask questions like “what was the date a week ago?” without doing calendar arithmetic by hand.

The Operations, Summarised

The small set of operations that this part needs:

OperationPurpose
date.fromisoformat(s)Turn a YYYY-MM-DD string into a date
date.today()Get today’s date as a date
d.isoformat()Turn a date back into a YYYY-MM-DD string
d1 < d2, d1 == d2, etc.Compare two dates
(d2 - d1).daysNumber of days between two dates
d + timedelta(days=n)The date n days after d
d - timedelta(days=n)The date n days before d

That is the whole list. Everything in this part is built from these.

The next short sequence asks you to practise the date arithmetic operations directly before applying them to rows.

Selecting Sessions in a Date Window

Now the first real application. A summary like “how many minutes did I study in the last seven days?” needs to select sessions whose dates fall inside a window. The window is bounded by a start date and an end date, and the convention this part uses throughout is that both ends are inclusive:

start <= session_day <= end

For “the last seven days ending today,” the window is:

end = today
start = today - timedelta(days=6)

Notice the 6, not 7. Today and the previous six days make seven days total. Off-by-one errors hide in this kind of arithmetic; writing the rule out plainly is the cheapest defence.

Inclusive date window

0 / 5 points

A function keeps events whose dates are inside an inclusive window. Which condition matches that contract?

The function that selects sessions in a window:

from datetime import date
def sessions_between(sessions, start, end):
"""Return sessions whose dates fall between start and end (both inclusive).
`start` and `end` are date objects. Sessions with invalid date strings
are skipped silently. Returns [] for empty input.
"""
result = []
for subject, minutes, day_string in sessions:
try:
day = date.fromisoformat(day_string)
except ValueError:
continue
if start <= day <= end:
result.append([subject, minutes, day_string])
return result

A few small things to notice. The function takes date objects for the bounds, not strings — the caller is responsible for parsing. The try/except around date.fromisoformat defends against rows whose date column is not valid ISO; those rows are skipped. The comparison start <= day <= end is Python’s chained comparison, which is the same as start <= day and day <= end.

A test exercising the boundary:

from datetime import date, timedelta
sessions = [
["math", 45, "2026-05-06"], # 6 days before today
["math", 30, "2026-05-12"], # today
["math", 60, "2026-05-05"], # 7 days before today -- out of window
]
today = date.fromisoformat("2026-05-12")
start = today - timedelta(days=6)
in_window = sessions_between(sessions, start, today)
assert len(in_window) == 2
assert in_window[0][2] == "2026-05-06" # included on the boundary
assert in_window[1][2] == "2026-05-12" # today is included

The boundary test matters. 2026-05-06 is exactly six days before 2026-05-12; the inclusive rule keeps it in. 2026-05-05 is seven days before; the rule excludes it. Both boundaries are tested explicitly so the rule cannot drift unnoticed.

Below, you get to prompt an AI so that it would generate a helper function.

Prompt an inclusive event window

0 / 10 points

Write a prompt that asks a large language model to create Python code only.

The generated code must define a function named events_between(events, start, end) with this exact behavior:

  • events is a list of rows where each row is [name, amount, date_string],
  • start and end are date objects from Python’s datetime module,
  • date_string is intended to be an ISO date such as "2026-04-15",
  • parse each row’s date string with date.fromisoformat(...),
  • include rows where start <= event_day <= end,
  • include rows on the start date,
  • include rows on the end date,
  • skip rows whose date string is invalid,
  • preserve the original row objects and their original order,
  • return an empty list when no rows match,
  • do not call input(...),
  • and do not print anything.

Example call:

from datetime import date
events = [
["open", 5, "2026-04-01"],
["middle", 8, "2026-04-03"],
["bad", 9, "April 4"],
["close", 2, "2026-04-05"],
]
events_between(events, date(2026, 4, 1), date(2026, 4, 3))

Required return value:

[
["open", 5, "2026-04-01"],
["middle", 8, "2026-04-03"],
]

Your prompt should explicitly ask for Python code only, with no explanation.

Example: total_in_last_seven_days

Once sessions_between exists, the “minutes in the last week” summary is short:

def total_in_last_seven_days(sessions, today=None):
"""Return total minutes from today and the previous six days.
`today` defaults to date.today() and is accepted as a parameter
so that tests can pin it down.
"""
if today is None:
today = date.today()
start = today - timedelta(days=6)
recent = sessions_between(sessions, start, today)
total = 0
for subject, minutes, day in recent:
total += minutes
return total

Two things worth pointing out. The function takes a today=None parameter and falls back to date.today() — this is the pattern that lets tests pin down a specific reference date without hard-coding the system clock. Every summary in this part that needs “today” follows the same convention.

The function reuses sessions_between from above, then totals the minutes from whatever came back.

Check Your Understanding

  1. What does date.fromisoformat("2026-05-10") return, and how is it different from the string "2026-05-10"?
  2. What is the value of (date.fromisoformat("2026-05-15") - date.fromisoformat("2026-05-10")).days?
  3. The chapter says the seven-day window uses start = today - timedelta(days=6). Why 6 and not 7?

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