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
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.
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
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.
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
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
timedelta accepts a weeks= argument: timedelta(weeks=n).
Adding that timedelta to d returns the shifted date.
from datetime import date, timedelta
def add_weeks(d, n):
return d + timedelta(weeks=n)
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)
Subtracting two dates gives a timedelta; .days is the count.
January has 31 days, February 2026 has 28: 31 + 28 = 59.
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")
%m is the month (01-12); %M is the minute.
A plain date has no minutes, so %M always gives 00 — use lowercase %m.
from datetime import date
def iso(d):
return d.strftime("%Y-%m-%d")
What does date(2026, 1, 10) - date(2026, 1, 1) return?
Subtracting two dates yields a timedelta (whose .days is 9), not a bare integer. To get the number, read the .days attribute. Adding two dates, by contrast, is what raises TypeError.
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
target - start gives a timedelta.
Read its .days attribute and return that integer.
from datetime import date
def days_until(target, start):
return (target - start).days
Recap
- Build explicit values with
date(2026, 1, 15)anddatetime(2026, 1, 15, 9, 30); avoidnow()where you need reproducibility. - A
timedeltais 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. strftimeformats a date to text,strptimeparses text to a date;%mis month,%Mis minute.isoformat()gives the sortable, standard2026-01-15form.
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.