Learning Goals
3 minBy the end of this lesson you can:
- Get today's date with
date.today()and the current moment withdatetime.now(). - Build a specific date with
date(2026, 12, 31)and subtract two dates to get atimedelta. - Format a date with
.strftime()for human-readable output. - Parse a string back into a date with
.strptime().
Warm-Up · Three Tiny Questions
5 minType these three lines and predict each output:
from datetime import date, datetime print(date.today()) print(datetime.now()) print(date(2026, 12, 31) - date(2026, 5, 27))
Show the answer (your dates will vary)
2026-05-27 2026-05-27 18:42:17.123456 218 days, 0:00:00
Today's date in ISO format. The current moment with microseconds. The gap between two dates as a timedelta.
Dates aren't strings — they're date objects. You can subtract them, format them, parse strings into them. Once they're objects, Python does the boring arithmetic.
New Concept · Four Types
14 min1 · date — a calendar day, no time
from datetime import date today = date.today() birthday = date(2014, 8, 12) # year, month, day print(today) # → 2026-05-27 print(today.year, today.month, today.day)
2 · datetime — date + time
from datetime import datetime now = datetime.now() print(now) # → 2026-05-27 18:42:17.123456 print(now.hour, now.minute, now.second)
3 · timedelta — a span of time
Subtract two dates and you get a timedelta. You can also build one directly to add or subtract.
from datetime import date, timedelta today = date.today() birthday = date(2014, 8, 12) age = today - birthday print(age.days, "days alive") # Add 30 days to today future = today + timedelta(days=30) print(future)
timedelta accepts days, seconds, weeks, minutes, hours — but not months or years (those are ambiguous without a fixed start date).
4 · strftime — format a date as a string
The funny name = "string format time". The first argument is a template string with %-codes.
today = date.today() print(today.strftime("%d %B %Y")) # → 27 May 2026 print(today.strftime("%A")) # → Wednesday print(today.strftime("%d/%m/%Y")) # → 27/05/2026 print(datetime.now().strftime("%I:%M %p")) # → 06:42 PM
The most useful codes %Y four-digit year 2026 %m month, padded 05 %d day, padded 27 %B month name May %A weekday name Wednesday %H hour 24h 18 %I hour 12h 06 %M minute, padded 42 %S second, padded 17 %p AM / PM PM
5 · strptime — parse a string
The reverse direction. "String parse time" — take a string + template, return a datetime.
from datetime import datetime raw = "27/05/2026" when = datetime.strptime(raw, "%d/%m/%Y") print(when) # → 2026-05-27 00:00:00 print(when.date()) # → 2026-05-27
Wrong template = ValueError. Same as every other parse — wrap in try/except for user input.
The ISO format · everywhere's favourite
YYYY-MM-DD is the international standard. It sorts correctly as a string, has no ambiguity, and Python loves it.
today = date.today() print(today.isoformat()) # → 2026-05-27 (same as str(today)) print(date.fromisoformat("2026-12-31")) # → 2026-12-31
Use ISO for file storage. Use strftime only when you're showing dates to humans.
Worked Example · The Birthday Counter
12 minSave birthday.py:
# birthday.py — tell me about my birthday from datetime import date, timedelta def age_in_days(birthday): return (date.today() - birthday).days def next_birthday(birthday): today = date.today() this_year_bday = date(today.year, birthday.month, birthday.day) if this_year_bday >= today: return this_year_bday return date(today.year + 1, birthday.month, birthday.day) def report(birthday): today = date.today() nb = next_birthday(birthday) days_left = (nb - today).days print(f"Today is : {today.strftime('%A, %d %B %Y')}") print(f"Birthday : {birthday.strftime('%A, %d %B %Y')}") print(f"Age in days : {age_in_days(birthday)}") print(f"Next birthday : {nb.strftime('%A, %d %B %Y')}") if days_left == 0: print("🎉 HAPPY BIRTHDAY! 🎉") else: print(f"Sleeps until : {days_left}") report(date(2014, 8, 12))
Sample output (run on 2026-05-27)
Today is : Wednesday, 27 May 2026 Birthday : Tuesday, 12 August 2014 Age in days : 4307 Next birthday : Wednesday, 12 August 2026 Sleeps until : 77
Read the diff
Two helper functions. age_in_days uses date subtraction to get a timedelta, then .days as an integer. next_birthday tries this year first; if it's already past, it bumps to next year. Both are pure functions — no I/O — so the same logic could be re-used in any app that needs "next anniversary of X".
Try It Yourself
13 minPrint today's date in three formats: ISO (2026-05-27), British (27/05/2026), and Long English (Wednesday, 27 May 2026).
Hint
from datetime import date t = date.today() print(t.isoformat()) print(t.strftime("%d/%m/%Y")) print(t.strftime("%A, %d %B %Y"))
Print the number of days until the next 25 December.
Hint
from datetime import date today = date.today() xmas = date(today.year, 12, 25) if xmas < today: xmas = date(today.year + 1, 12, 25) print((xmas - today).days, "days until Christmas")
Same shape as the birthday: try this year, bump to next year if it's gone.
Ask the user for a date in dd/mm/yyyy format and print how many days from today (positive if future, negative if past).
Hint
from datetime import datetime, date raw = input("Date (dd/mm/yyyy): ") try: when = datetime.strptime(raw, "%d/%m/%Y").date() except ValueError: print("Bad date format.") raise SystemExit diff = (when - date.today()).days if diff > 0: print(f"{when} is {diff} days in the future.") elif diff < 0: print(f"{when} was {-diff} days ago.") else: print("That's today!")
Parsing user dates is a classic real-world chore. The format string must match exactly, character for character — single mistake = ValueError.
Mini-Challenge · The Timestamped Activity Log
8 minBuild activity_log.py. The user types short notes; each is saved with a timestamp. A second mode reads back the log.
Your file must:
- Define
log(note)that appends a line toactivity.logwith format2026-05-27T18:42:17 | note text. - Define
show(n=5)that prints the lastnentries, formatted nicely (usestrptimeto parse the timestamp andstrftimeto reformat). - Define
since(days)that prints all entries newer thandaysdays ago. - A small menu loop ties them together.
Show one possible solution
# activity_log.py — timestamped notes with parsing back from datetime import datetime, timedelta FILE = "activity.log" def log(note): stamp = datetime.now().isoformat(timespec="seconds") with open(FILE, "a", encoding="utf-8") as f: f.write(f"{stamp} | {note}\n") def all_entries(): try: with open(FILE, encoding="utf-8") as f: entries = [] for line in f: if " | " not in line: continue stamp_s, note = line.rstrip("\n").split(" | ", 1) stamp = datetime.fromisoformat(stamp_s) entries.append((stamp, note)) return entries except FileNotFoundError: return [] def show(n=5): for stamp, note in all_entries()[-n:]: print(f" {stamp.strftime('%a %d %b %H:%M')} — {note}") def since(days): cutoff = datetime.now() - timedelta(days=days) for stamp, note in all_entries(): if stamp >= cutoff: print(f" {stamp.isoformat(timespec='minutes')} — {note}") while True: pick = input("\n1 log 2 show 3 since-days 4 quit: ") if pick == "1": log(input("Note: ")) elif pick == "2": show() elif pick == "3": since(int(input("Days: "))) elif pick == "4": break
Non-negotiables: timestamps stored in ISO format (sorts correctly!), parsed back with fromisoformat, and strftime only when printing to a human. The since(days) filter uses timedelta arithmetic on datetime.now() — the cleanest possible "last 7 days" check.
Recap
3 minFour types: date, datetime, timedelta, and the duo strftime/strptime. Subtract two dates → a timedelta. Add a timedelta to a date → a future or past date. Use ISO format (YYYY-MM-DD) for storing dates — it sorts correctly and there's no ambiguity. Use strftime("%A %d %B %Y") only for showing dates to humans. Wrap strptime in try/except for user input.
Vocabulary Card
- date.today()
- Today's calendar date, no time.
- datetime.now()
- The current moment — date and time and microseconds.
- timedelta
- A span between two moments. Get one by subtracting; build one with
timedelta(days=N). - strftime
- Format a date/datetime as a string. The first argument is a template.
- strptime
- Parse a string into a datetime. Mirror of strftime — same template codes.
- ISO format
YYYY-MM-DDfor dates,YYYY-MM-DDTHH:MM:SSfor datetimes. The international standard.
Homework
4 minBuild age_inspector.py. The user types a date of birth as dd/mm/yyyy. Your program prints:
- Age in years (use integer division:
(today - dob).days // 365as a rough estimate). - Age in days.
- The day of the week they were born (
%A). - How many sleeps until their next birthday.
- The exact date of their 18th birthday — and how many days until/since it.
Stretch. Handle Feb 29 birthdays gracefully — there's no Feb 29 in non-leap years.
Sample · age_inspector.py
# age_inspector.py — facts about a birthday from datetime import date, datetime raw = input("Your DOB (dd/mm/yyyy): ") try: dob = datetime.strptime(raw, "%d/%m/%Y").date() except ValueError: print("Bad date.") raise SystemExit today = date.today() delta = today - dob print(f"Born on : {dob.strftime('%A, %d %B %Y')}") print(f"Age (rough) : {delta.days // 365} years") print(f"Age in days : {delta.days}") # Next birthday try: nb = date(today.year, dob.month, dob.day) except ValueError: # Feb 29 in non-leap year — bump to Feb 28 nb = date(today.year, dob.month, 28) if nb < today: try: nb = date(today.year + 1, dob.month, dob.day) except ValueError: nb = date(today.year + 1, dob.month, 28) print(f"Next birthday : {nb.strftime('%A, %d %B %Y')} ({(nb - today).days} sleeps)") # 18th birthday try: eighteen = date(dob.year + 18, dob.month, dob.day) except ValueError: eighteen = date(dob.year + 18, dob.month, 28) gap = (eighteen - today).days if gap >= 0: print(f"18th birthday : {eighteen} ({gap} days to go)") else: print(f"18th birthday : {eighteen} ({-gap} days ago)")
Non-negotiables: parse with strptime, multiple strftime outputs, a next-birthday calculation. The Feb-29 handling uses a try/except ValueError on the date() constructor — that's the kind of edge case Level-2 students are now ready to handle.