Lesson 7 · Python Beginner Course
Python datetime Tutorial: Dates, Time Zones & Reminders (Beginner Python Course, Part 7)
In this lesson you'll be able to: Create and compare dates, times and datetimes with the datetime module Turn text into dates and dates into friendly text…
- CoursePython Beginner Course
- Lesson7 of 10
- Video56 min
- FormatJupyter notebook · 84 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbDates and Times: Calendars, Time Zones and Reminders#
In this lesson you'll be able to:
- Create and compare dates, times and datetimes with the
datetimemodule - Turn text into dates and dates into friendly text with
strptimeandstrftime - Do date maths with
timedelta: ages, countdowns and working days - Use the
calendarmodule for month views, leap years and paydays - Handle time zones and daylight saving with
zoneinfo, plus timestamps and timing code - Capstone: build a birthday and bill reminder planner that reads a CSV and shows the next 30 days
Warm-Up#
Counting days by hand works... until a month ends. Let's see it break, then fix it for good with Python's datetime module.
last_watered = {"Basil": 28, "Fern": 25, "Cactus": 2} # day of the month
today_day = 30
for plant, day in last_watered.items():
print(f"{plant:<7} watered {today_day - day} days ago")
# Now it's the 3rd of the next month. Plain numbers get it badly wrong:
today_day = 3
for plant, day in last_watered.items():
print(f"{plant:<7} watered {today_day - day} days ago")
print("Negative days? We need real dates.")
The date Object#
A
dateis a page torn off a calendar: a year, a month and a day, nothing more. Python checks that the date is real and knows which weekday it falls on.
from datetime import date
launch = date(2026, 3, 14)
print(launch)
print(launch.year, launch.month, launch.day)
print(type(launch))
print(launch.weekday()) # Monday is 0, Sunday is 6
print(launch.isoweekday()) # Monday is 1, Sunday is 7
day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
print("Pi Day 2026 falls on a", day_names[launch.weekday()])
real_today = date.today() # this one changes every day you run it
print("Real today:", real_today)
print("Is it after Pi Day 2026?", real_today > launch)
deadline = date(2026, 4, 1)
print(launch < deadline)
print("Later of the two:", max(launch, deadline))
appointments = [date(2026, 5, 2), date(2026, 1, 20), date(2026, 3, 9)]
print(sorted(appointments))
for appointment in sorted(appointments):
print("Vet visit:", appointment)
moved = launch.replace(day=21)
print(launch, "->", moved)
print(date.fromisoformat("2026-07-04"))
Common mistake: a date that doesn't exist#
pay_day = date(2026, 2, 30)
def is_real_date(year, month, day):
try:
date(year, month, day)
return True
except ValueError:
return False
print(is_real_date(2026, 2, 30), is_real_date(2026, 2, 28), is_real_date(2028, 2, 29))
Times and Datetimes#
A
timeis the clock on the wall. Adatetimeis the calendar page and the clock together.
from datetime import datetime, time
flight = datetime(2026, 7, 18, 6, 45)
print(flight)
print(flight.date(), "|", flight.time())
print("Boarding hour:", flight.hour, "minute:", flight.minute)
opening = time(7, 30)
closing = time(18, 0)
print("Cafe hours:", opening, "to", closing)
order_time = time(17, 55)
print("Open for this order?", opening <= order_time < closing)
def cafe_status(t):
if t < opening:
return "not open yet"
if t >= closing:
return "closed for the day"
return "open, come in!"
for t in [time(6, 59), time(7, 30), time(12, 15), time(18, 0)]:
print(t.strftime("%H:%M"), "->", cafe_status(t))
dinner = datetime.combine(date(2026, 7, 18), time(19, 30))
print("Dinner booking:", dinner)
print("Same day as the flight?", dinner.date() == flight.date())
now = datetime.now() # changes every run
print("Right now:", now.replace(microsecond=0))
print(flight.isoformat())
print(datetime.fromisoformat("2026-07-18T06:45:00"))
Pro tip: store and share dates as ISO text (
2026-07-18). It sorts correctly even as plain strings.
Formatting Dates with strftime#
strftimemeans string from time: you give a pattern, Python fills in the blanks.
| Code | Means | Example |
|---|---|---|
%d %m %Y |
day, month, year | 05, 09, 2026 |
%A %a |
weekday name | Saturday, Sat |
%B %b |
month name | September, Sep |
%H:%M |
24-hour clock | 20:30 |
%I:%M %p |
12-hour clock | 08:30 PM |
gig = datetime(2026, 9, 5, 20, 30)
print(gig.strftime("%d/%m/%Y"))
print(gig.strftime("%A %d %B %Y"))
print(gig.strftime("%H:%M"))
print(gig.strftime("%I:%M %p"))
formats = {"UK": "%d/%m/%Y", "US": "%m/%d/%Y", "ISO": "%Y-%m-%d",
"Friendly": "%a %d %b", "Ticket": "%d %b %Y, doors %H:%M"}
for name, pattern in formats.items():
print(f"{name:<9}{gig.strftime(pattern)}")
print(f"Doors open at {gig:%H:%M} on {gig:%A}")
print(f"Ticket date: {gig:%d %b %Y}")
def nice_date(d):
if 11 <= d.day <= 13:
suffix = "th"
else:
suffix = {1: "st", 2: "nd", 3: "rd"}.get(d.day % 10, "th")
return f"{d:%A} {d.day}{suffix} {d:%B}"
for day in [1, 2, 3, 11, 22, 23]:
print(nice_date(date(2026, 9, day)))
Quick check#
What does date(2026, 12, 25).strftime("%a %d/%m") print?
print(date(2026, 12, 25).strftime("%a %d/%m"))
Reading Dates from Text with strptime#
strptimeis the reverse, string parse time: the pattern must match the text exactly.
text = "18/07/2026 06:45"
flight = datetime.strptime(text, "%d/%m/%Y %H:%M")
print(flight)
print(type(flight).__name__)
receipt_dates = ["03/02/2026", "14/02/2026", "28/02/2026"]
parsed = [datetime.strptime(t, "%d/%m/%Y").date() for t in receipt_dates]
print(parsed)
print("First:", parsed[0], " Last:", parsed[-1])
print(datetime.strptime("Sat 5 September 2026", "%a %d %B %Y").date())
print(datetime.strptime("8:15 PM", "%I:%M %p").time())
Common mistake: a pattern that doesn't match the text#
datetime.strptime("2026-07-18", "%d/%m/%Y")
def parse_any(text):
for pattern in ["%d/%m/%Y", "%Y-%m-%d", "%d %b %Y"]:
try:
return datetime.strptime(text.strip(), pattern).date()
except ValueError:
pass
return None
for t in ["18/07/2026", "2026-07-18", "18 Jul 2026", "July 18th"]:
print(f"{t:<12} -> {parse_any(t)}")
def ask_date(prompt):
while True:
answer = input(prompt)
found = parse_any(answer)
if found:
return found
print(f" Sorry, '{answer}' isn't a real date. Try 25/12/1990.")
birthday = ask_date("Your birthday (DD/MM/YYYY): ")
print("Got it:", nice_date(birthday), birthday.year)
Date Maths with timedelta#
A
timedeltais a length of time, like "14 days" or "8 hours 25 minutes". Dates move by adding them.
from datetime import timedelta
holiday = date(2026, 7, 18)
print("Back home:", holiday + timedelta(days=14))
print("Book by: ", holiday - timedelta(weeks=6))
gap = date(2026, 12, 25) - date(2026, 7, 18)
print(gap)
print(gap.days, "days until Christmas")
print(type(gap).__name__)
start = datetime(2026, 3, 9, 9, 15)
end = datetime(2026, 3, 9, 17, 40)
shift = end - start
print("Shift length:", shift)
print("In hours:", shift.total_seconds() / 3600)
first_collection = date(2026, 1, 6)
print("Recycling bin days:")
for n in range(6):
print(" ", (first_collection + timedelta(days=14 * n)).strftime("%a %d %b"))
print(date(2026, 1, 31) + timedelta(days=30)) # crosses the end of February
print(date(2028, 2, 28) + timedelta(days=1)) # 2028 is a leap year
print(date(2026, 12, 31) + timedelta(days=1)) # happy new year
Common mistake: adding a plain number to a date#
print(holiday + 14)
print(holiday + timedelta(days=14))
print(holiday + timedelta(days=1, hours=36)) # hours roll over into days, the date shows 2 days later
Ages, Countdowns and Working Days#
Most real date questions are one of three: how old, how long until, how many working days. We'll use a fixed "today" so the answers never change: Monday 9 March 2026.
TODAY = date(2026, 3, 9)
def age_on(born, on_day):
years = on_day.year - born.year
if (on_day.month, on_day.day) < (born.month, born.day):
years -= 1 # birthday not reached yet this year
return years
print(age_on(date(1995, 3, 14), TODAY))
print(age_on(date(1995, 3, 14), date(2026, 3, 14)))
def next_birthday(born, today):
upcoming = born.replace(year=today.year)
if upcoming < today:
upcoming = upcoming.replace(year=today.year + 1)
return upcoming
nb = next_birthday(birthday, TODAY)
print("Next birthday:", nice_date(nb), nb.year)
print("Days to go:", (nb - TODAY).days)
print("You'll be turning", age_on(birthday, nb))
countdowns = {"Summer holiday": date(2026, 7, 18), "Gig": date(2026, 9, 5),
"Christmas": date(2026, 12, 25), "Tax return": date(2027, 1, 31)}
for event, when in countdowns.items():
days = (when - TODAY).days
print(f"{event:<15}{days:>4} days ({days // 7} weeks)")
def working_days(start, end):
count = 0
day = start
while day < end:
if day.weekday() < 5: # Mon to Fri
count += 1
day += timedelta(days=1)
return count
print("Working days until the holiday:", working_days(TODAY, date(2026, 7, 18)))
bank_holidays = {date(2026, 4, 3), date(2026, 4, 6), date(2026, 5, 4)}
def add_working_days(start, n, holidays=()):
day = start
while n > 0:
day += timedelta(days=1)
if day.weekday() < 5 and day not in holidays:
n -= 1
return day
invoice = date(2026, 3, 30)
print("Pay within 10 working days:", add_working_days(invoice, 10))
print("Counting bank holidays: ", add_working_days(invoice, 10, bank_holidays))
Your turn#
- Work out your own age in days:
(TODAY - your_birthday).days. - Find how many working days are left in March 2026 from
TODAY.
Mini-Project: The Plant Watering Calendar#
Back to the warm-up, this time with real dates. Each plant has a last watering day and a rhythm.
plants = {
"Basil": {"last": date(2026, 3, 7), "every": 2},
"Fern": {"last": date(2026, 3, 4), "every": 4},
"Cactus": {"last": date(2026, 2, 20), "every": 21},
"Orchid": {"last": date(2026, 3, 2), "every": 7},
}
print(len(plants), "plants on the windowsill")
def next_watering(info):
return info["last"] + timedelta(days=info["every"])
for name, info in plants.items():
print(f"{name:<7} next due {next_watering(info):%a %d %b}")
def watering_status(info, today):
days_left = (next_watering(info) - today).days
if days_left < 0:
return f"OVERDUE by {-days_left} day(s)"
if days_left == 0:
return "water today"
return f"in {days_left} day(s)"
for name, info in plants.items():
print(f"{name:<7} {watering_status(info, TODAY)}")
by_due = sorted(plants.items(), key=lambda pair: next_watering(pair[1]))
print("Watering order:", [name for name, info in by_due])
print(f"Watering plan for the week of {TODAY:%d %B}")
for offset in range(7):
day = TODAY + timedelta(days=offset)
thirsty = [n for n, i in plants.items() if (day - i["last"]).days % i["every"] == 0 and day > i["last"]]
print(f" {day:%a %d}: {', '.join(thirsty) or '-'}")
The calendar Module#
Python ships with a whole wall calendar: month views, leap years and day names. Handy for paydays, bin days and anything that says "the last Friday of the month".
import calendar
print(calendar.month(2026, 12))
print("2026 leap year?", calendar.isleap(2026))
print("2028 leap year?", calendar.isleap(2028))
print("Leap years from 2000 to 2029:", calendar.leapdays(2000, 2030))
print("February 2026 (first weekday, days):", calendar.monthrange(2026, 2))
print(list(calendar.day_name))
print(calendar.month_name[12], "|", calendar.month_abbr[12])
def last_weekday(year, month, weekday):
days_in_month = calendar.monthrange(year, month)[1]
day = date(year, month, days_in_month)
while day.weekday() != weekday:
day -= timedelta(days=1)
return day
print("Payday is the last Friday of the month:")
for month in range(1, 7):
print(f" {calendar.month_abbr[month]}: {last_weekday(2026, month, calendar.FRIDAY):%d %b}")
Quick check#
How many Friday the 13ths are there in 2026? Guess, then run the cell.
unlucky = [date(2026, m, 13) for m in range(1, 13) if date(2026, m, 13).weekday() == calendar.FRIDAY]
print(len(unlucky), "of them:", [d.strftime("%B") for d in unlucky])
Time Zones with zoneinfo#
A datetime without a zone is naive: it doesn't know where on Earth it is. Add
tzinfoand it becomes aware. Pro tip: on Windows, runpip install tzdataonce sozoneinfohas its time zone database.
from zoneinfo import ZoneInfo
london = ZoneInfo("Europe/London")
call = datetime(2026, 7, 18, 15, 0, tzinfo=london)
print(call)
print(call.tzname(), call.utcoffset())
cities = {"New York": "America/New_York", "Tokyo": "Asia/Tokyo",
"Sydney": "Australia/Sydney", "Nairobi": "Africa/Nairobi"}
print(f"Team call at {call:%H:%M} in London is:")
for city, zone in cities.items():
local = call.astimezone(ZoneInfo(zone))
print(f" {city:<9}{local:%a %H:%M} ({local.tzname()})")
naive = datetime(2026, 7, 18, 15, 0)
print("Naive zone:", naive.tzinfo)
aware = naive.replace(tzinfo=london)
print("Aware zone:", aware.tzinfo, aware.utcoffset())
Common mistake: mixing naive and aware datetimes#
deadline = datetime(2026, 7, 18, 17, 0)
print(call < deadline)
deadline = datetime(2026, 7, 18, 17, 0, tzinfo=london)
print("Call before the deadline?", call < deadline)
from datetime import timezone
server_time = datetime(2026, 7, 18, 14, 0, tzinfo=timezone.utc)
print("Server (UTC):", server_time)
print("London: ", server_time.astimezone(london))
zones = {"london": london, "new york": ZoneInfo("America/New_York"),
"tokyo": ZoneInfo("Asia/Tokyo"), "sydney": ZoneInfo("Australia/Sydney")}
while True:
city = input("Convert the call to which city? ").strip().lower()
if city in zones:
break
print(" I know:", ", ".join(zones))
print(f"In {city.title()} the call is at {call.astimezone(zones[city]):%H:%M on %A}")
Your turn#
- Add Paris (
"Europe/Paris") and your own city to thezonesdictionary. - Find a time on a weekday when it is between 09:00 and 17:00 in both London and New York.
Daylight Saving: When an Hour Goes Missing#
In the UK, clocks jump from 01:00 to 02:00 on 29 March 2026 and fall back on 25 October.
for when in [datetime(2026, 1, 15, 12, 0), datetime(2026, 7, 15, 12, 0)]:
local = when.replace(tzinfo=london)
print(f"{local:%d %b}: {local.tzname()}, UTC offset {local.utcoffset()}")
bedtime = datetime(2026, 3, 28, 22, 0, tzinfo=london)
alarm = datetime(2026, 3, 29, 6, 0, tzinfo=london)
print("Looks like:", alarm - bedtime, "of sleep")
real = alarm.astimezone(timezone.utc) - bedtime.astimezone(timezone.utc)
print("Really got:", real, "of sleep")
print("Bedtime in UTC:", bedtime.astimezone(timezone.utc).strftime("%H:%M"))
print("Alarm in UTC: ", alarm.astimezone(timezone.utc).strftime("%H:%M"))
Pro tip: do your maths in UTC, and convert to local time only when you show it to a person.
Quick check#
What will
tzname()say for London on 1 November 2026: GMT or BST?
print(datetime(2026, 11, 1, 12, 0, tzinfo=london).tzname())
Timestamps: Time as One Big Number#
A timestamp counts seconds since 1 January 1970 (UTC). Apps, logs and databases love them. It's the same number everywhere on Earth, so turn it into local time only when you display it.
print(datetime.fromtimestamp(0, tz=timezone.utc))
moment = datetime(2026, 7, 18, 12, 0, tzinfo=timezone.utc)
print(moment.timestamp())
import time
print("Seconds since 1970, right now:", round(time.time())) # changes every run
workout_log = [1784358000, 1784361720, 1784444400, 1784447100] # from a fitness watch
for stamp in workout_log:
when = datetime.fromtimestamp(stamp, tz=london)
print(stamp, "->", when.strftime("%a %d %b %H:%M"))
sessions = [(workout_log[0], workout_log[1]), (workout_log[2], workout_log[3])]
for start, end in sessions:
minutes = (end - start) / 60
print(f"Workout on {datetime.fromtimestamp(start, tz=london):%A}: {minutes:.0f} minutes")
Pausing and Timing: time.sleep and time.perf_counter#
sleepis a pause button.perf_counteris a stopwatch.
for n in [3, 2, 1]:
print(n, "...")
time.sleep(0.3)
print("Kettle's on!")
start = time.perf_counter()
total = sum(range(1_000_000))
elapsed = time.perf_counter() - start
print("Total:", total)
print(f"Took {elapsed:.4f} seconds (your number will differ)")
def stopwatch(label, task):
start = time.perf_counter()
task()
took = time.perf_counter() - start
print(f"{label:<14}{took * 1000:8.2f} ms")
return took
nap = stopwatch("short nap", lambda: time.sleep(0.25))
print("Close to 0.25 s?", abs(nap - 0.25) < 0.1)
guest_list = [f"guest{n}" for n in range(200_000)]
guest_set = set(guest_list)
t_list = stopwatch("list lookup", lambda: "guest199999" in guest_list)
t_set = stopwatch("set lookup", lambda: "guest199999" in guest_set)
print("The set was faster:", t_set < t_list)
Pro tip: use
perf_counterfor measuring how long code takes.datetime.now()can jump if the computer's clock is adjusted.Your turn#
- Write a 5-second countdown that prints the seconds remaining each time it sleeps.
- Time how long it takes to sort a list of 100,000 shuffled numbers.
Capstone: The Birthday and Bill Reminder Planner#
Everything together: a CSV of events, parsing, date maths, calendar, a time zone and a stopwatch.
Our fixed "today" is Thursday 10 December 2026, so the window crosses New Year.
Step 1: Save and load the events#
One row per event. repeat is yearly, monthly or once; bills also have an amount in dollars.
from pathlib import Path
folder = Path("lesson_files")
folder.mkdir(exist_ok=True)
events_csv = folder / "events.csv"
events_csv.write_text(
"name,kind,date,repeat,amount\n"
"Mum,birthday,1961-12-14,yearly,\n"
"Leo,birthday,2019-01-03,yearly,\n"
"Priya,birthday,1994-06-21,yearly,\n"
"Grandad,birthday,1948-12-25,yearly,\n"
"Amara,birthday,1990-12-28,yearly,\n"
"Rent,bill,2026-01-01,monthly,850.00\n"
"Phone,bill,2026-01-18,monthly,22.50\n"
"Gym,bill,2026-01-31,monthly,35.00\n"
"Car insurance,bill,2026-01-05,yearly,410.00\n"
"TV licence,bill,2026-12-20,once,174.50\n"
"Dentist,appointment,2026-12-10,once,\n"
"Passport photos,appointment,2026-11-30,once,\n"
)
print(len(events_csv.read_text().splitlines()) - 1, "events saved to", events_csv)
import csv
with open(events_csv, newline="") as f:
rows = list(csv.DictReader(f))
print(rows[0])
print(rows[5])
from dataclasses import dataclass
@dataclass
class Event:
name: str
kind: str
start: date
repeat: str
amount: float = 0.0
events = [Event(r["name"], r["kind"], date.fromisoformat(r["date"]), r["repeat"],
float(r["amount"] or 0)) for r in rows]
print(events[6])
Step 2: When is each event next due?#
Birthdays come round every year, bills every month. Careful: not every month has a 31st.
def same_day_in_month(day, year, month):
last_day = calendar.monthrange(year, month)[1]
return date(year, month, min(day, last_day)) # 31st becomes 30th or 28th
print(same_day_in_month(31, 2027, 2))
print(same_day_in_month(31, 2026, 12))
def next_occurrence(event, today):
if event.repeat == "once":
return event.start if event.start >= today else None
if event.repeat == "yearly":
return next_birthday(event.start, today)
when = same_day_in_month(event.start.day, today.year, today.month)
if when < today:
year, month = (today.year + 1, 1) if today.month == 12 else (today.year, today.month + 1)
when = same_day_in_month(event.start.day, year, month)
return when
print("Rent from 9 March 2026:", next_occurrence(Event("Rent", "bill", date(2026, 1, 1), "monthly"), TODAY))
PLANNER_TODAY = date(2026, 12, 10)
for e in events[5:8] + events[10:]:
print(f"{e.name:<16}{e.repeat:<8} -> {next_occurrence(e, PLANNER_TODAY)}")
Step 3: The 30-day report#
Keep only what falls inside the window, sort it by date, then print one friendly line per reminder.
def due_within(events, today, days=30):
window_end = today + timedelta(days=days)
due = []
for e in events:
when = next_occurrence(e, today)
if when is not None and when <= window_end:
due.append((when, e))
return sorted(due, key=lambda pair: pair[0])
coming_up = due_within(events, PLANNER_TODAY)
print(len(coming_up), "things in the next 30 days")
def describe_gap(days):
if days == 0:
return "TODAY"
if days == 1:
return "tomorrow"
return f"in {days} days"
print([describe_gap(n) for n in [0, 1, 12]])
def reminder_line(when, e, today):
gap = describe_gap((when - today).days)
line = f"{when:%a %d %b} {gap:<12}{e.name:<14}"
if e.kind == "birthday":
line += f"turns {age_on(e.start, when)}"
elif e.kind == "bill":
line += f"${e.amount:,.2f}"
if when.weekday() >= 5:
line += " (weekend: pay Friday)"
else:
line += e.kind
return line
print(reminder_line(*coming_up[0], PLANNER_TODAY))
def print_report(events, today, days=30):
due = due_within(events, today, days)
print(f"=== Next {days} days from {today:%A %d %B %Y} ===")
for when, e in due:
print(reminder_line(when, e, today))
bills = sum(e.amount for when, e in due if e.kind == "bill")
print(f"--- {len(due)} reminders, ${bills:,.2f} of bills to pay ---")
print_report(events, PLANNER_TODAY)
Step 4: Extras#
Weekly bill totals, a marked calendar, a call to a friend abroad, a saved file and a stopwatch.
print("Bills by ISO week:")
weekly = {}
for when, e in coming_up:
if e.kind == "bill":
week = when.isocalendar().week
weekly[week] = weekly.get(week, 0) + e.amount
for week, total in weekly.items():
print(f" Week {week:>2}: ${total:>8,.2f}")
marked = {when.day for when, e in coming_up if when.month == 12}
print("December 2026 (* = something due)")
print("".join(f"{name:>3} " for name in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]))
for week in calendar.monthcalendar(2026, 12):
print("".join(" " if d == 0 else f"{d:>3}{'*' if d in marked else ' '}" for d in week))
sydney = ZoneInfo("Australia/Sydney")
amara_bday = next_occurrence(events[4], PLANNER_TODAY)
call_sydney = datetime(amara_bday.year, amara_bday.month, amara_bday.day, 9, 0, tzinfo=sydney)
call_london = call_sydney.astimezone(london)
print(f"Amara lives in Sydney. 9am on her birthday there is")
print(f"{call_london:%A %d %B at %H:%M} in London ({call_london.tzname()})")
def ask_days():
while True:
answer = input("How many days ahead should I look? ")
if answer.isdigit() and 1 <= int(answer) <= 366:
return int(answer)
print(" Please type a whole number of days, like 14.")
days = ask_days()
print_report(events, PLANNER_TODAY, days)
reminders_file = folder / "reminders.txt"
lines = [reminder_line(when, e, PLANNER_TODAY) for when, e in due_within(events, PLANNER_TODAY)]
reminders_file.write_text("\n".join(lines) + "\n")
print("Saved", len(lines), "reminders to", reminders_file)
print(reminders_file.read_text().splitlines()[0])
start = time.perf_counter()
for offset in range(365):
due_within(events, PLANNER_TODAY + timedelta(days=offset))
took = time.perf_counter() - start
print(f"Planned a whole year of reminders in {took:.3f} seconds (yours will differ)")
Recap#
date,timeanddatetimeobjects;weekday(),replace(),combine()and ISO formatstrftimeturns dates into text;strptimeturns text into dates (the pattern must match)timedeltafor adding, subtracting and measuring gaps;total_seconds()- Ages, countdowns and working days with small, testable functions
calendar: month views,isleap,monthrange, day and month nameszoneinfo: aware vs naive datetimes,astimezone, daylight saving, UTC for maths- Timestamps with
timestamp()andfromtimestamp() time.sleepto pause andtime.perf_counterto measure
Try it yourself#
- Add a
weeklyrepeat type (bin day, gym class) tonext_occurrenceand the CSV. - Handle 29 February birthdays: in non-leap years, celebrate on 28 February.
- Let each event have its own time zone column, and show reminders in both local and London time.
What's next#
Lesson 8: Automate Your Files and Folders. Put Python to work tidying folders, renaming files in bulk and making timestamped backups.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



