Module 8 · Python Under the Hood ⏱ 18 min

Working with Dates and Times

By the end of this lesson you will be able to:
  • Construct date and datetime values from explicit, deterministic parts
  • Use timedelta to shift dates across month and leap-year boundaries
  • Format dates to text and parse text back to dates with strftime and strptime

Dates look like simple data until you try to do anything with them. Subtract two dates and you reasonably expect a count of days — and you need to know which object you actually get back. Add a month and you have to remember that months hold different numbers of days, that February leaps every four years (mostly), and that adding thirty days to the end of January does not land on the last day of February. The same confusion appears when you compare times across regions, where the same moment is afternoon in one city and morning in another.

Hand-rolled calendar arithmetic is where quiet bugs live. The datetime module does this arithmetic correctly so you do not have to. It gives you objects that represent a date, a time, or both, plus a timedelta type for durations, and the usual operators work on them the way you would hope.

flowchart TD
  D["date: a calendar day"] --> DT["datetime: date plus time"]
  T["time: a clock, no day"] --> DT
  TD["timedelta: a duration"] -.->|"add or subtract"| DT
  style D fill:#3776ab,color:#fff
  style TD fill:#b45309,color:#fff
Four building blocks: date (a calendar day), time (a clock), datetime (both), and timedelta (a duration you add or subtract).

Constructing values

Bring the types in by name, then build them with explicit parts. A date takes a year, month, and day; a datetime adds hours, minutes, and the rest.

from datetime import date, datetime

d = date(2026, 1, 15)              # January 15, 2026
dt = datetime(2026, 1, 15, 9, 30)  # 9:30am on that day

These constructors take fixed numbers, so the result is fully deterministic — the same arguments always build the same date. That matters in tests. Those same parts are readable back through attributes like .year and .month, which you will see in the example below. The module also offers date.today() and datetime.now(), which read the system clock; useful in real programs but non-deterministic, so prefer explicit construction whenever a value has to be reproducible.

Build a date and a datetime, then read their parts back through attributes.
from datetime import date, datetime

d = date(2026, 1, 15)
dt = datetime(2026, 1, 15, 9, 30)

print(d)
print(dt)
print("year:", d.year, "month:", d.month, "day:", d.day)

timedelta: a duration you can add

A timedelta represents an amount of time — seven days, twelve hours, thirty minutes — rather than a moment on the calendar. Add one to a date and you get a new date; the module handles the month boundaries and leap years for you.

from datetime import date, timedelta

date(2026, 1, 30) + timedelta(days=10)   # 2026-02-09

That single line crossed from January into February correctly, because the date object knows January has thirty-one days. Negative shifts work the same way: subtracting timedelta(days=10) moves a date ten days earlier, again crossing boundaries without complaint. The same arithmetic written by hand would need a table of month lengths and a special case for leap years — exactly the fiddly code the module spares you.

flowchart LR
  A["date(2026, 1, 30)"] -->|"+ timedelta(days=10)"| B["2026-02-09"]
  A -.->|"naive +10 days"| W["2026-01-40 broken"]
  style B fill:#3776ab,color:#fff
  style W fill:#b45309,color:#fff
Adding a timedelta shifts a date across a month boundary correctly — the part hand-rolled code gets wrong.
Date arithmetic that crosses a month boundary without any special-casing.
from datetime import date, timedelta

start = date(2026, 1, 30)
print("start:", start)
print("+10 days:", start + timedelta(days=10))
print("-10 days:", start - timedelta(days=10))

The difference between two dates is a timedelta

Subtraction works too. Take one date from another and you get a timedelta whose .days attribute holds the count of days between them.

date(2026, 3, 1) - date(2026, 1, 1)   # timedelta(days=59)

In 2026, January has thirty-one days and February has twenty-eight, so the gap is fifty-nine days. Notice the asymmetry: subtracting two dates makes sense and yields a duration, but adding two dates does not — that raises a TypeError. You can add or subtract a date and a timedelta, and you can subtract two dates, but never two dates together.

Format a date to text with strftime, then parse text back to a date with strptime.
from datetime import date, datetime

d = date(2026, 1, 15)
print(d.strftime("%Y-%m-%d"))                 # to string

back = datetime.strptime("2026-01-15", "%Y-%m-%d").date()
print(back == d)                              # back to a date

ISO format: the shape that sorts

date.isoformat() prints the standard form 2026-01-15 — year, month, day, in that order, zero-padded. That ordering is not arbitrary: because the most significant part comes first, ISO dates sort correctly even when stored as plain text. A list of ISO strings in alphabetical order is also in chronological order, which makes them ideal for filenames, log entries, and anything you might later sort. The same standard orders time the same way, which is why databases, logs, and configuration files converge on it. Reach for ISO unless you have a specific reason to display a locale-specific format.

flowchart LR
  DT["a date object"] -->|"strftime %Y-%m-%d"| S["2026-01-15"]
  S -->|"strptime %Y-%m-%d"| DT
  style DT fill:#3776ab,color:#fff
  style S fill:#b45309,color:#fff
strftime turns a date into text; strptime turns text back into a date. Format codes bridge the two directions.
Exercise

Write add_weeks(d, n) that takes a date d and an integer n, and returns a new date that is n weeks later. Use a timedelta.

from datetime import date, timedelta

def add_weeks(d, n):
    # your code here
    pass
Exercise

How many days? Predict the single printed number. 2026 is not a leap year.

from datetime import date

print((date(2026, 3, 1) - date(2026, 1, 1)).days)
Exercise

This function should format a date as YYYY-MM-DD, but it prints 00 where the month should be — it used the minute code by mistake. Fix the format string.

from datetime import date

def iso(d):
    return d.strftime("%Y-%M-%d")
Exercise

What does date(2026, 1, 10) - date(2026, 1, 1) return?

Exercise

Write days_until(target, start) that takes two date objects and returns the number of days from start to target as an integer. A target on the same day returns 0.

from datetime import date

def days_until(target, start):
    # your code here
    pass

Recap

  • Build explicit values with date(2026, 1, 15) and datetime(2026, 1, 15, 9, 30); avoid now() where you need reproducibility.
  • A timedelta is a duration; adding or subtracting one from a date crosses month and leap-year boundaries correctly.
  • Subtracting two dates yields a timedelta — read .days. You cannot add two dates.
  • strftime formats a date to text, strptime parses text to a date; %m is month, %M is minute.
  • isoformat() gives the sortable, standard 2026-01-15 form.

Next you will step outside the language itself with os and sys, the modules for reading paths and the runtime environment your program lives in.

Checkpoint quiz

Which type does timedelta(days=7) create?

Why does date(2026, 1, 30) + timedelta(days=10) give 2026-02-09 and not 2026-01-40?

Go deeper — technical resources