103. Datetime Module
Date arithmetic and formatting with datetime
103. Datetime Module
Each of Rohan’s RAM reports needs a unique timestamp in the filename and a human-readable date in its content.
He tries to compute “how many hours ago was this snapshot?” and runs into the timedelta trap:
from datetime import datetime
snapshot_time = datetime.fromisoformat("2026-03-07T14:30:00")
now = datetime.now()
diff = now - snapshot_time # this is a timedelta, NOT a number
print(diff) # "0:45:22" — hours:minutes:seconds string
print(diff.seconds) # 2722 — seconds in the current day
print(diff.total_seconds()) # 2722.0 — total seconds as floatFor filenames, he needs strftime with the right format codes:
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
# → "20260307_143022" (safe for filenames — no spaces or colons)
readable = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# → "2026-03-07 14:30:22" (ISO 8601 — the universal standard)He also computes when the next scheduled report should run:
from datetime import timedelta
last_report = date.fromisoformat("2026-03-01")
next_report = last_report + timedelta(days=7)
# → date(2026, 3, 8) — timedelta arithmetic returns a new date objectdate vs datetime: Use
datewhen you only care about the calendar day (no time). Usedatetimewhen you need both. Subtracting twodateobjects gives atimedelta— you must access.daysto get an integer.
💡 Fun fact: Python’s datetime module was added in Python 2.3 (2003). The ISO 8601 date format (YYYY-MM-DD) was defined by the International Organization for Standardization in 1988. It’s used globally because it sorts lexicographically — "2026-03-07" > "2026-02-28" is True even as plain strings, which makes date-based filenames sort correctly without any parsing.
⚠️ Watch out: datetime.now() returns the local system time with no timezone info. If your code runs on servers in different timezones, always use datetime.now(timezone.utc) or datetime.utcnow() for consistent timestamps. Mixing timezone-aware and timezone-naive datetimes raises TypeError.
🤔 Think about it: timedelta.days gives you the number of complete days in a timedelta, but timedelta.total_seconds() gives you the total duration as seconds. If the timedelta is 2 days and 3 hours, what does .days return vs what does .total_seconds() / 3600 return? When would using .days give you the wrong answer?
Learning objectives
- Subtract two date objects and extract the integer day count using .days
- Format dates as ISO 8601 strings with strftime(“%Y-%m-%d”)
- Parse ISO date strings with date.fromisoformat()
- Add or subtract timedelta to compute future/past dates
Key concepts
- date and datetime types
- timedelta — result of date subtraction
- timedelta.days — extract integer days
- strftime() — format date to string
- date.fromisoformat() — parse ISO strings
Try it
Concept detail
The datetime Module
Python’s datetime module handles dates, times, and arithmetic between them. The most common types are date (year/month/day) and datetime (date + time).
Date Arithmetic with timedelta
Subtracting two date objects returns a timedelta object — not a plain integer. You must access the .days attribute to get a usable number:
from datetime import date, timedelta
d1 = date(2026, 1, 1)
d2 = date(2026, 3, 7)
diff = d2 - d1 # timedelta(days=65)
print(diff) # 65 days, 0:00:00
print(diff.days) # 65 ← the integer you wantFormatting with strftime
Use .strftime() to format a date as a string. ISO 8601 uses dashes:
d = date(2026, 3, 7)
d.strftime("%Y-%m-%d") # "2026-03-07" ← ISO format (universal)
d.strftime("%Y/%m/%d") # "2026/03/07" ← NOT ISO format
d.strftime("%B %d, %Y") # "March 07, 2026"Key Reference
| Method / Class | Description |
|---|---|
date.today() | Current local date |
date.fromisoformat("2026-03-07") | Parse ISO date string |
date(year, month, day) | Construct a date directly |
timedelta(days=N) | Duration of N days |
timedelta.days | Integer days from a timedelta |
.strftime(fmt) | Format date as string |
datetime.strptime(s, fmt) | Parse string to datetime |
Solution
from datetime import date, timedelta
def days_since(last_review_str: str) -> int:
"""Return number of days since the given date string (YYYY-MM-DD)."""
last_review = date.fromisoformat(last_review_str)
today = date(2026, 3, 7) # fixed date for deterministic tests
diff = today - last_review
return diff.days # .days extracts integer from timedelta
def format_date(d: date) -> str:
"""Format a date as YYYY-MM-DD ISO string."""
return d.strftime("%Y-%m-%d") # dashes for ISO 8601 format
def is_overdue(last_review_str: str, interval_days: int = 7) -> bool:
"""Return True if the topic hasn't been reviewed within interval_days."""
days = days_since(last_review_str)
return days > interval_days
def next_review_date(last_review_str: str, interval_days: int = 7) -> str:
"""Return the next review date as an ISO string."""
last_review = date.fromisoformat(last_review_str)
next_date = last_review + timedelta(days=interval_days)
return format_date(next_date)Tests
def test_days_since_returns_integer():
result = days_since("2026-02-28")
assert isinstance(result, int), f"days_since should return int, got {type(result)}"
def test_days_since_correct_value():
# 2026-03-07 minus 2026-02-28 = 7 days
result = days_since("2026-02-28")
assert result == 7, f"Expected 7 days, got {result}"
def test_days_since_zero():
# Same day should be 0
result = days_since("2026-03-07")
assert result == 0, f"Expected 0 days for today, got {result}"
def test_days_since_longer_gap():
# 2026-03-07 minus 2026-01-01 = 65 days
result = days_since("2026-01-01")
assert result == 65, f"Expected 65, got {result}"
def test_format_date_uses_dashes():
d = date(2026, 3, 7)
result = format_date(d)
assert result == "2026-03-07", f"Expected '2026-03-07', got '{result}'"
def test_format_date_no_slashes():
d = date(2026, 1, 15)
result = format_date(d)
assert "/" not in result, f"format_date should use dashes not slashes, got '{result}'"
def test_is_overdue_true():
# 30 days ago with default 7-day interval → overdue
result = is_overdue("2026-02-05")
assert result is True, "Topic reviewed 30 days ago should be overdue"
def test_is_overdue_false():
# 3 days ago with default 7-day interval → not overdue
result = is_overdue("2026-03-04")
assert result is False, "Topic reviewed 3 days ago should not be overdue"
def test_next_review_date_format():
result = next_review_date("2026-03-01", 7)
assert result == "2026-03-08", f"Expected '2026-03-08', got '{result}'"