← Home

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 float

For 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 object

date vs datetime: Use date when you only care about the calendar day (no time). Use datetime when you need both. Subtracting two date objects gives a timedelta — you must access .days to 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 want

Formatting 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 / ClassDescription
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.daysInteger 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}'"

Resources