The first time I had to parse a timestamp from an API, I spent twenty minutes reaching for third-party libraries before remembering Python ships with exactly what I need. The datetime module is part of the standard library — no pip install, no external dependency. After that moment I started using it by default, and I have not needed dateutil or pendulum for anything production-facing in years.
This article walks through the datetime module end to end. You will learn how to build date, time, and datetime objects, perform arithmetic with timedelta, convert between strings and datetime types, and handle timezone-aware datetimes with zoneinfo. Every code block is runnable as-is on Python 3.11 and above.
TLDR
- date, time, and datetime are three distinct classes — datetime is the one most people use most often
- timedelta represents a duration and works with +, -, and comparison operators on date and datetime objects
- strftime formats a datetime as a string; strptime parses a string into a datetime — both use the same format codes
- zoneinfo (Python 3.9+) attaches timezone context to a datetime; without it the datetime is “naive” and raises TypeError when compared to an aware datetime
- date and datetime objects are immutable — modifying them always returns a new instance rather than mutating in place
What is the datetime module in Python?
The datetime module is a built-in standard library for representing and manipulating dates and times. It provides four primary classes: date for calendar dates, time for times of day, datetime for combined date-time values, and timedelta for durations. All four are immutable — methods that look like they modify an object instead return a new one.
date, time, and datetime objects
A date object represents a calendar date — year, month, and day — with no time component. A time object represents a time of day independent of any date. A datetime combines both into a single object and is the most commonly used class for representing a specific moment. All three classes expose their components as read-only attributes. When you need to combine a date and a time that live in separate objects, datetime.combine() joins them into a single datetime.
import datetime
# date object -- calendar date only
d = datetime.date(2026, 3, 15)
print(d)
print(d.year, d.month, d.day)
print(datetime.date.today())
# time object -- time of day only
t = datetime.time(14, 25, 0)
print(t)
print(t.hour, t.minute, t.second)
print(datetime.time.min, datetime.time.max)
# datetime object -- date and time combined
dt = datetime.datetime(2026, 3, 15, 14, 25, 0)
print(dt)
print(dt.date())
print(dt.time())
# Combine a date object and a time object into one datetime
scheduled_date = datetime.date(2026, 8, 20)
scheduled_time = datetime.time(17, 0, 0)
meeting = datetime.datetime.combine(scheduled_date, scheduled_time)
print(meeting)
# Replace the time component of an existing datetime
dt2 = datetime.datetime(2026, 8, 20, 9, 30, 0)
evening = dt2.replace(hour=17, minute=0, second=0)
print(evening)
# Current moments
now = datetime.datetime.now()
utc_now = datetime.datetime.utcnow()
print(now)
print(utc_now)
# From Unix timestamp
ts = datetime.datetime.fromtimestamp(1747500000)
print(ts)
2026-03-15
2026 3 15
2026-05-16
14:25:00
14 25 0
00:00:00 23:59:59.999999
2026-03-15 14:25:00
2026-03-15
14:25:00
2026-08-20 17:00:00
2026-08-20 17:00:00
2026-05-16 09:30:00
2026-05-16 09:00:00
2026-05-16 09:00:00
All arguments to date and time constructors are optional and default to 0. datetime.now() returns the current local time based on the system clock, and datetime.utcnow() returns the current UTC time without any timezone information attached. Understanding how Python’s immutable types work explains why these objects behave like values rather than references.
timedelta: arithmetic with durations
timedelta represents a duration — the difference between two dates or datetimes. It is constructed with keyword arguments: days, seconds, microseconds, milliseconds, minutes, and hours. You add and subtract timedeltas from date and datetime objects to shift them forward or backward in time.
import datetime
# Add 45 days to a project start date
start = datetime.date(2026, 2, 1)
deadline = start + datetime.timedelta(days=45)
print(deadline)
# How many days between two dates
d1 = datetime.date(2026, 1, 1)
d2 = datetime.date(2026, 12, 31)
diff = d2 - d1
print(diff.days)
print(diff.total_seconds())
# A 90-minute meeting starting at 10:00
meeting_start = datetime.datetime(2026, 5, 1, 10, 0, 0)
meeting_end = meeting_start + datetime.timedelta(hours=1, minutes=30)
print(meeting_end)
# A week ago from today
today = datetime.date.today()
last_week = today - datetime.timedelta(weeks=1)
print(last_week)
2026-03-18
364
31449600.0
2026-05-01 11:30:00
2026-05-09
The .days attribute returns an integer number of days. The .total_seconds() method returns the full duration as a floating-point number of seconds, which is useful for converting to other units or storing in a data file. Multiplying a timedelta by an integer scales the duration, and dividing one timedelta by another gives a pure float ratio between two durations. The built-in pow function works with numeric values in the same way timedelta works with time values — combining primitives to produce a result.
strftime and strptime: datetime and strings
strftime (“string from time”) converts a datetime object into a string using a format template. strptime (“parse from string”) does the reverse — it takes a string and a format pattern and returns a datetime object. Both use the same set of format codes to describe the layout of the date or time string.
import datetime
dt = datetime.datetime(2026, 7, 14, 9, 30, 0)
# Format as different string styles
print(dt.strftime("%Y-%m-%d"))
print(dt.strftime("%d/%m/%Y"))
print(dt.strftime("%B %d, %Y at %H:%M"))
print(dt.strftime("%A, %B %d")) # e.g. "Tuesday, July 14"
print(dt.strftime("Week %W, %Y")) # week number of year
# Parse a string back into a datetime
date_str = "14-07-2026 09:30"
fmt = "%d-%m-%Y %H:%M"
parsed = datetime.datetime.strptime(date_str, fmt)
print(parsed)
2026-07-14
14/07/2026
July 14, 2026 at 09:30
Tuesday, July 14
Week 28, 2026
2026-07-14 09:30:00
The most commonly used format codes are: %Y (four-digit year), %m (zero-padded month), %d (zero-padded day), %H (24-hour hour), %M (minute), %S (second), %A (weekday name), and %B (month name). The full list is in the Python documentation. If the input string does not match the format pattern exactly — including any literal characters like hyphens or colons — Python raises a ValueError. Python string formatting covers the format mini-language used by f-strings and .format(), which is a separate but related technique.
Time zones with zoneinfo
Python 3.9 introduced zoneinfo, a timezone-aware datetime class built into the standard library. A datetime without tzinfo is called naive. A datetime with tzinfo is called aware. The critical rule: Python raises a TypeError if you compare an aware datetime with a naive one. zoneinfo resolves this by attaching IANA timezone identifiers — like “America/New_York” or “Europe/London” — to datetime objects.
import datetime
from zoneinfo import ZoneInfo
# Create an aware datetime in UTC
utc_dt = datetime.datetime(2026, 7, 14, 12, 0, tzinfo=ZoneInfo("UTC"))
print("UTC:", utc_dt)
# Convert UTC to Eastern Time (New York)
ny_dt = utc_dt.astimezone(ZoneInfo("America/New_York"))
print("NY: ", ny_dt)
# Convert the same UTC moment to Tokyo
tokyo_dt = utc_dt.astimezone(ZoneInfo("Asia/Tokyo"))
print("Tokyo:", tokyo_dt)
# Attach a timezone to a naive datetime
naive = datetime.datetime(2026, 7, 14, 12, 0)
aware = naive.replace(tzinfo=ZoneInfo("UTC"))
print("Aware:", aware)
# The TypeError case -- comparing naive and aware raises TypeError
naive_dt = datetime.datetime(2026, 6, 1, 12, 0)
aware_dt = datetime.datetime(2026, 6, 1, 12, 0, tzinfo=ZoneInfo("UTC"))
# Uncommenting the next line raises TypeError:
# print(naive_dt < aware_dt)
# Fix: make the naive datetime aware first
fixed = naive_dt.replace(tzinfo=ZoneInfo("UTC"))
print(fixed < aware_dt) # True -- both are now UTC-aware
UTC: 2026-07-14 12:00:00+00:00
NY: 2026-07-14 08:00:00-04:00
Tokyo: 2026-07-14 21:00:00+09:00
Aware: 2026-07-14 12:00:00+00:00
True
The .utcoffset() method returns a timedelta representing the UTC offset, and .tzname() returns a string like “UTC” or “EST”. The most common cause of the “can’t compare offset-naive and offset-aware datetimes” TypeError is mixing datetimes from zoneinfo with those from datetime.now() or datetime.utcnow(), which produce naive objects by default. The fix is to pass tz=ZoneInfo(“UTC”) to datetime.now() so it returns an aware datetime from the start.
Unix timestamps
A Unix timestamp is the number of seconds elapsed since January 1, 1970, at 00:00:00 UTC (the “epoch”). Python’s datetime module converts between this integer representation and datetime objects with fromtimestamp() and timestamp(). This integer form is useful for logging, caching, and data exchange across systems with different local time conventions.
import datetime
dt = datetime.datetime(2026, 1, 1, 0, 0, 0)
unix = dt.timestamp()
print(unix)
# Round-trip back to datetime
recovered = datetime.datetime.fromtimestamp(unix)
print(recovered)
# UTC-aware round-trip avoids local-time interpretation
unix_utc = datetime.datetime(2026, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc).timestamp()
print(unix_utc)
recovered_utc = datetime.datetime.fromtimestamp(unix_utc, tz=datetime.timezone.utc)
print(recovered_utc)
1767225600.0
2026-01-01 00:00:00
1767225600.0
2026-01-01 00:00:00+00:00
FAQ
Q: What is the difference between datetime.now() and datetime.utcnow()?
datetime.now() returns the current local time based on the machine’s system clock. datetime.utcnow() returns the current UTC time but does not attach any timezone object to the result — the returned datetime is still naive. For new code, the recommended approach is datetime.now(tz=ZoneInfo(“UTC”)) which produces an aware UTC datetime that can be compared with other aware datetimes from any timezone.
Q: How do I calculate the number of days between two dates?
Subtract one date from the other. The result is a timedelta object. Call .days on it to get the number of days as an integer. For business days (excluding weekends), loop over each day in the range and skip Saturdays and Sundays using the .weekday() method.
Q: How do I add months to a date in Python?
The datetime module does not have a direct method for month arithmetic. Adding 30 days as a proxy for a month works in most cases but fails near month boundaries — adding 30 days to January 31 produces March 2 or 3 depending on the year. The dateutil.relativedelta class handles calendar month arithmetic correctly by respecting the target month’s length.
Q: What is a Unix timestamp?
A Unix timestamp is the number of seconds since January 1, 1970, at 00:00:00 UTC. datetime.fromtimestamp() converts a timestamp to a local datetime. datetime.timestamp() does the reverse for a datetime object. The integer form is useful for logging, caching, and data exchange where a compact, timezone-neutral integer is preferred.
Q: Why does comparing two datetimes raise a TypeError?
The error “can’t compare offset-naive and offset-aware datetimes” occurs when one datetime has a tzinfo attribute and the other does not. Call .replace(tzinfo=ZoneInfo(“UTC”)) on the naive datetime before comparing, or use datetime.now(tz=ZoneInfo(“UTC”)) instead of datetime.now() to always produce aware datetimes from the start.
Q: How do I get the day of the week from a date?
The .weekday() method on date and datetime objects returns an integer from 0 (Monday) to 6 (Sunday). The .isoweekday() method returns 1 (Monday) through 7 (Sunday), matching the ISO calendar convention used by most business systems.
The datetime module handles the overwhelming majority of date and time tasks without third-party libraries. For intervals use timedelta. For string conversion use strftime and strptime. For timezone-aware work use zoneinfo. These three patterns — construct, convert, compare — cover nearly every real-world scenario. Python functions and Python operators cover the complementary language primitives for working with data in Python.

