Working with dates and times is notoriously tricky. Timezones, daylight saving time, and different formats create subtle bugs. Python’s datetime module handles most of these cases, and understanding its design prevents common mistakes.
The datetime module
The datetime module provides several types:
from datetime import date, time, datetime, timedelta, timezone
date— a calendar date (year, month, day)time— a time of day (hour, minute, second, microsecond)datetime— a combined date and timetimedelta— a duration (difference between two datetimes)timezone— timezone information
Creating datetimes
from datetime import date, datetime
# Specific date
birthday = date(1990, 5, 15)
# date(1990, 5, 15)
# Specific datetime
meeting = datetime(2025, 4, 6, 14, 30)
# datetime(2025, 4, 6, 14, 30)
# Current date/time
today = date.today()
now = datetime.now() # naive — no timezone
now_utc = datetime.now(timezone.utc) # aware — UTC timezone
Accessing components
meeting = datetime(2025, 4, 6, 14, 30)
meeting.year # 2025
meeting.month # 4
meeting.day # 6
meeting.hour # 14
meeting.minute # 30
meeting.weekday() # 0 — Monday (0=Monday, 6=Sunday)
Formatting and parsing
Convert a datetime to a string:
now = datetime.now()
now.strftime("%Y-%m-%d %H:%M") # "2025-04-06 14:30"
now.strftime("%B %d, %Y") # "April 06, 2025"
now.isoformat() # "2025-04-06T14:30:00.000000"
Parse a string into a datetime:
dt = datetime.strptime("2025-04-06 14:30", "%Y-%m-%d %H:%M")
dt = datetime.fromisoformat("2025-04-06T14:30:00")
Common format codes:
| Code | Meaning | Example |
|------|---------|---------|
| %Y | 4-digit year | 2025 |
| %m | 2-digit month | 04 |
| %d | 2-digit day | 06 |
| %H | 24-hour | 14 |
| %M | Minute | 30 |
| %S | Second | 00 |
| %B | Full month name | April |
| %b | Abbreviated month | Apr |
| %A | Full weekday name | Sunday |
Calculating differences
Subtract two datetimes to get a timedelta:
from datetime import datetime, timedelta
start = datetime(2025, 4, 1)
end = datetime(2025, 4, 6, 14, 30)
diff = end - start
print(diff) # 5 days, 14:30:00
print(diff.days) # 5
print(diff.seconds) # 52200 (14*3600 + 30*60)
print(diff.total_seconds()) # 484200.0
Add or subtract timedeltas:
now = datetime.now()
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(weeks=1)
in_one_hour = now + timedelta(hours=1)
timedelta supports: days, seconds, microseconds, milliseconds, minutes, hours, weeks.
Naive vs aware datetimes
A naive datetime has no timezone information:
naive = datetime(2025, 4, 6, 14, 30)
naive.tzinfo # None
An aware datetime includes timezone:
from datetime import timezone, timedelta as td
utc = timezone.utc
aware = datetime(2025, 4, 6, 14, 30, tzinfo=utc)
aware.tzinfo # datetime.timezone.utc
UTC as the standard
Store and transmit datetimes in UTC:
from datetime import datetime, timezone
now_utc = datetime.now(timezone.utc)
print(now_utc) # 2025-04-06 14:30:00+00:00
Convert to a local timezone for display:
from datetime import timezone, timedelta
# Create a timezone for EST (UTC-5)
est = timezone(timedelta(hours=-5))
local_time = now_utc.astimezone(est)
print(local_time) # 2025-04-06 09:30:00-05:00
For proper timezone handling with daylight saving time, use the zoneinfo module from the standard library (Python 3.9+):
from datetime import datetime
from zoneinfo import ZoneInfo
now_utc = datetime.now(timezone.utc)
ny_time = now_utc.astimezone(ZoneInfo("America/New_York"))
london_time = now_utc.astimezone(ZoneInfo("Europe/London"))
zoneinfo handles DST transitions automatically.
A real-world example: age calculator
from datetime import date
def calculate_age(birthdate):
"""Calculate age in years from a birthdate."""
today = date.today()
age = today.year - birthdate.year
# Adjust if birthday has not occurred this year
if (today.month, today.day) < (birthdate.month, birthdate.day):
age = age - 1
return age
age = calculate_age(date(1990, 5, 15))
print(f"Age: {age}")
A real-world example: scheduling helper
from datetime import datetime, timedelta
def next_occurrence(day_of_week, hour, minute, tz=None):
"""Calculate the next occurrence of a given day and time.
Args:
day_of_week: 0=Monday, 6=Sunday.
hour: Hour in 24-hour format.
minute: Minute.
tz: Timezone (defaults to local).
Returns:
The next datetime matching the day and time.
"""
now = datetime.now(tz)
days_ahead = day_of_week - now.weekday()
if days_ahead <= 0:
# Target day already occurred this week — go to next week
days_ahead = days_ahead + 7
target = now + timedelta(days=days_ahead)
target = target.replace(hour=hour, minute=minute, second=0, microsecond=0)
return target
# Next Monday at 9:00 AM
next_monday = next_occurrence(0, 9, 0)
print(f"Next Monday 9 AM: {next_monday}")
Common date/time mistakes
-
Comparing naive and aware datetimes — Python raises a
TypeError. Always ensure both datetimes are either naive or aware. -
Forgetting that
timedeltadoes not support months or years — months have variable lengths. Use thedateutillibrary for month arithmetic. -
Storing datetimes in ambiguous formats — store as UTC datetime objects or ISO 8601 strings with timezone offset (e.g.,
"2025-04-06T14:30:00+00:00"). Avoid formats like"April 6, 2025"that cannot be reliably parsed back or lack timezone information. -
Ignoring DST transitions — naive arithmetic across DST boundaries can be off by an hour. Use
zoneinfofor timezone-aware calculations.
What to carry forward
datetimecombines date and time;dateandtimeare separate types- use
.strftime()to format; use.strptime()to parse - subtract datetimes to get a
timedelta - add
timedeltato datetimes for date arithmetic - prefer timezone-aware datetimes — use UTC internally
- use
zoneinfofor proper timezone handling (Python 3.9+) - naive and aware datetimes cannot be compared
timedeltadoes not support months or years — usedateutilfor that
Dates and times are a common source of subtle bugs. Awareness and discipline around timezones prevent most of them. The next lesson covers testing — how to verify your code works correctly.