Warm-up · Activity 1 of 7
Warm-up from the regular expressions lesson: what does this print?
import re
print(re.search(r"\d{4}-\d{2}-\d{2}", "Invoice from 2026-09-29, due soon").group())// I4.4 · ~30 min · Intermediate
After this lesson you can read and write dates, calculate with them using timedelta, and handle times in UTC without mixing naive and aware datetimes.
You will be able to
Warm-up · Activity 1 of 7
import re
print(re.search(r"\d{4}-\d{2}-\d{2}", "Invoice from 2026-09-29, due soon").group())Predict · Activity 2 of 7
from datetime import date, timedelta
print(date(2026, 9, 29) + timedelta(days=3))Practice · Activity 3 of 7
text = d.strftime("%d.%m.____")Practice · Activity 4 of 7
from datetime import date
print((date(2026, 12, 24) - date(2026, 9, 29)).days)Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
from datetime import timedelta
duration = timedelta(hours=36)
print(duration.days, duration.seconds)Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
The program reads deadlines in ISO format, sorts them, and says how many days are left from a fixed today (a real program would use date.today()). Then it reads two log times in UTC, one ending in Z and one in +00:00, and works out how long the job ran.
main.py
from datetime import date, datetime, timedelta
today = date(2026, 9, 29) # in a real program: date.today()
deadlines = {"tax return": "2026-10-31", "passport": "2026-09-15", "report": "2026-10-02"}
for task, text in sorted(deadlines.items(), key=lambda item: item[1]):
due = date.fromisoformat(text)
left = (due - today).days
status = "overdue" if left < 0 else f"{left} days left"
print(f"{task:<11}{due.strftime('%a %d.%m.%Y')} {status}")
# Two times from a server log, in UTC.
start = datetime.fromisoformat("2026-09-29T08:15:00Z")
end = datetime.fromisoformat("2026-09-29T10:05:30+00:00")
print(end - start, (end - start).total_seconds())
print(start.tzinfo, start.isoformat())
print((start + timedelta(weeks=2)).date())
Run it with
python main.pyOutput
passport Tue 15.09.2026 overdue
report Fri 02.10.2026 3 days left
tax return Sat 31.10.2026 32 days left
1:50:30 6630.0
UTC 2026-09-29T08:15:00+00:00
2026-10-13Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercise 1 of 2
Dates come as ISO strings. Write due_date(borrowed, weeks=3), which returns the return date as an ISO string; days_overdue(due, returned), which returns how many days late a book came back, or 0 when it was on time; and german(iso), which formats a date as 05.10.2026.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
date.fromisoformat turns the text into a date, and .isoformat() turns a date back into text.
Add timedelta(weeks=weeks). For days_overdue, subtract the due date from the return date and take .days.
max(late, 0) turns a negative number of days into 0. strftime("%d.%m.%Y") writes the German form.
One way to solve it. Yours can look different and still pass the checks.
from datetime import date, timedelta
def due_date(borrowed: str, weeks: int = 3) -> str:
return (date.fromisoformat(borrowed) + timedelta(weeks=weeks)).isoformat()
def days_overdue(due: str, returned: str) -> int:
late = (date.fromisoformat(returned) - date.fromisoformat(due)).days
return max(late, 0)
def german(iso: str) -> str:
return date.fromisoformat(iso).strftime("%d.%m.%Y")
if __name__ == "__main__":
print(due_date("2026-09-29"), days_overdue("2026-10-20", "2026-10-23"), german("2026-10-05"))
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from datetime import date, timedelta
def due_date(borrowed: str, weeks: int = 3) -> str:
return borrowed
def days_overdue(due: str, returned: str) -> int:
return 0
def german(iso: str) -> str:
return iso
if __name__ == "__main__":
print(due_date("2026-09-29"), days_overdue("2026-10-20", "2026-10-23"), german("2026-10-05"))
test_main.py
from main import days_overdue, due_date, german
def test_due_date():
"""Three weeks after 2026-09-29 is 2026-10-20"""
got = due_date("2026-09-29")
assert got == "2026-10-20", f"due_date returned {got!r}"
def test_due_date_new_year():
"""Two weeks after 2026-12-20 is in the next year"""
got = due_date("2026-12-20", 2)
assert got == "2027-01-03", f"due_date('2026-12-20', 2) returned {got!r}"
def test_overdue():
"""Returned 3 days late gives 3, returned early gives 0"""
got = days_overdue("2026-10-20", "2026-10-23"), days_overdue("2026-10-20", "2026-10-01")
assert got == (3, 0), f"days_overdue returned {got!r}, expected (3, 0)"
def test_german():
"""2026-10-05 is written 05.10.2026"""
got = german("2026-10-05")
assert got == "05.10.2026", f"german returned {got!r}"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
Log times arrive in mixed form: some with an offset such as Z or +02:00, some without, which the logs mean as UTC. Write parse_utc(text), which returns an aware datetime in UTC for both kinds. Then write minutes_between(start, end), which returns the whole minutes between two such texts. Mixed forms must not raise a TypeError.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
After fromisoformat, check moment.tzinfo: it is None for a text without an offset.
A naive time that means UTC gets its zone with moment.replace(tzinfo=timezone.utc).
An aware time in another offset is converted with moment.astimezone(timezone.utc). Then any two results can be subtracted.
One way to solve it. Yours can look different and still pass the checks.
from datetime import datetime, timezone
def parse_utc(text: str) -> datetime:
moment = datetime.fromisoformat(text)
if moment.tzinfo is None:
return moment.replace(tzinfo=timezone.utc)
return moment.astimezone(timezone.utc)
def minutes_between(start: str, end: str) -> int:
return int((parse_utc(end) - parse_utc(start)).total_seconds() // 60)
if __name__ == "__main__":
print(parse_utc("2026-09-29T10:00:00+02:00"))
print(minutes_between("2026-09-29T08:15:00Z", "2026-09-29 10:05"))
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from datetime import datetime, timezone
def parse_utc(text: str) -> datetime:
return datetime.fromisoformat(text)
def minutes_between(start: str, end: str) -> int:
return int((parse_utc(end) - parse_utc(start)).total_seconds() // 60)
if __name__ == "__main__":
print(parse_utc("2026-09-29T10:00:00+02:00"))
print(minutes_between("2026-09-29T08:15:00Z", "2026-09-29 10:05"))
test_main.py
from datetime import datetime, timezone
from main import minutes_between, parse_utc
def test_naive_becomes_utc():
"""A time without an offset is read as UTC"""
got = parse_utc("2026-09-29 08:15")
assert got == datetime(2026, 9, 29, 8, 15, tzinfo=timezone.utc), f"parse_utc returned {got!r}"
assert got.tzinfo == timezone.utc, f"the result has tzinfo {got.tzinfo!r}, expected timezone.utc"
def test_offset_converted():
"""10:00 at +02:00 is 08:00 in UTC"""
got = parse_utc("2026-09-29T10:00:00+02:00")
assert (got.hour, got.tzinfo) == (8, timezone.utc), f"parse_utc returned {got!r}, expected 08:00 in UTC"
def test_mixed_minutes():
"""08:15Z to 10:05 without an offset is 110 minutes"""
got = minutes_between("2026-09-29T08:15:00Z", "2026-09-29 10:05")
assert got == 110, f"minutes_between returned {got!r}"
def test_over_midnight():
"""23:30 to 00:10 the next day is 40 minutes"""
got = minutes_between("2026-09-29T23:30:00+00:00", "2026-09-30T00:10:00+00:00")
assert got == 40, f"minutes_between returned {got!r}"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyfrom datetime import datetime, timezone
local = datetime(2026, 9, 29, 12, 0)
server = datetime(2026, 9, 29, 12, 0, tzinfo=timezone.utc)
print(local < server)
What Python prints
TypeError: can't compare offset-naive and offset-aware datetimesWhy, and the fix
local has no time zone, so Python cannot tell which moment it means, and refuses to order it against an exact moment. Decide what the naive value means. If it is UTC, write local.replace(tzinfo=timezone.utc); better, create aware values from the start with tzinfo=timezone.utc.
from datetime import date
birthday = date.fromisoformat("29.09.2026")
What Python prints
ValueError: Invalid isoformat string: '29.09.2026'Why, and the fix
fromisoformat only reads ISO 8601, year first: 2026-09-29. For other layouts, give the format: datetime.strptime("29.09.2026", "%d.%m.%Y").date(). Store and exchange dates in ISO form, and use strftime only for display.
from datetime import date
tomorrow = date(2026, 9, 29) + 1
What Python prints
TypeError: unsupported operand type(s) for +: 'datetime.date' and 'int'Why, and the fix
A plain 1 does not say whether you mean a day, an hour or a week. Add a timedelta instead: date(2026, 9, 29) + timedelta(days=1).
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.