Working with Dates, Times and Timezones in Python: datetime, pytz and zoneinfo
Python ships the datetime module in its standard library and, since Python 3.9, the zoneinfo module for timezone-aware code without external dependencies. For Python 3.8 compatibility, pytz remains the most popular third-party option.
1. datetime basics: creating and comparing
from datetime import date, time, datetime, timedelta
# Today and now
today = date.today()
now = datetime.now()
print(today) # 2025-06-15
print(now) # 2025-06-15 14:30:22.123456
# Specific date/time
meeting = datetime(2025, 9, 1, 10, 30, 0)
print(f"Meeting: {meeting}")
# Compare
print(meeting > now) # True or False
# Difference → timedelta
delta = meeting - now
print(f"{delta.days} days and {delta.seconds // 3600} hours away")
2. Arithmetic with timedelta
from datetime import datetime, timedelta
now = datetime.now()
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(weeks=1)
in_two_hours = now + timedelta(hours=2, minutes=30)
print(tomorrow.strftime('%Y-%m-%d %H:%M'))
print(last_week.strftime('%Y-%m-%d'))
print(in_two_hours.strftime('%H:%M'))
# Exact age calculation
from datetime import date
def age(birth: date) -> int:
today = date.today()
return today.year - birth.year - (
(today.month, today.day) < (birth.month, birth.day)
)
print(age(date(1990, 3, 25))) # 35 (in 2025)
3. Parsing and formatting: strftime / strptime
from datetime import datetime
# strptime: string → datetime
text = "15/06/2025 14:30"
dt = datetime.strptime(text, "%d/%m/%Y %H:%M")
print(dt) # 2025-06-15 14:30:00
# strftime: datetime → string
print(dt.strftime("%A, %B %d, %Y")) # Sunday, June 15, 2025
print(dt.strftime("%Y-%m-%dT%H:%M:%S")) # ISO 8601
# Multiple real-world formats
formats = [
("20250615", "%Y%m%d"),
("Jun 15, 2025", "%b %d, %Y"),
("2025-06-15T14:30:00", "%Y-%m-%dT%H:%M:%S"),
("15-06-25", "%d-%m-%y"),
]
for text, fmt in formats:
dt = datetime.strptime(text, fmt)
print(f"{text!r:30} → {dt.date()}")
Common strftime codes
| Code | Meaning | Example |
|---|---|---|
%Y |
4-digit year | 2025 |
%m |
Month 01-12 | 06 |
%d |
Day 01-31 | 15 |
%H |
Hour 00-23 | 14 |
%M |
Minute 00-59 | 30 |
%S |
Second 00-59 | 00 |
%A |
Full weekday name | Sunday |
%B |
Full month name | June |
%f |
Microseconds | 000000 |
4. Timezones with zoneinfo (Python ≥ 3.9)
from datetime import datetime
from zoneinfo import ZoneInfo
tz_madrid = ZoneInfo("Europe/Madrid")
tz_ny = ZoneInfo("America/New_York")
tz_tokyo = ZoneInfo("Asia/Tokyo")
# Timezone-aware datetime
now_madrid = datetime.now(tz_madrid)
print(f"Madrid : {now_madrid.strftime('%Y-%m-%d %H:%M %Z%z')}")
# Convert between timezones
now_ny = now_madrid.astimezone(tz_ny)
now_tokyo = now_madrid.astimezone(tz_tokyo)
print(f"New York: {now_ny.strftime('%Y-%m-%d %H:%M %Z')}")
print(f"Tokyo : {now_tokyo.strftime('%Y-%m-%d %H:%M %Z')}")
# UTC → local
from datetime import timezone
utc_time = datetime(2025, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
local_time = utc_time.astimezone(tz_madrid)
print(f"UTC → Madrid: {local_time.strftime('%H:%M %Z')}") # 14:00 CEST
5. Timezones with pytz (Python 3.6-3.8)
import pytz
from datetime import datetime
tz_madrid = pytz.timezone("Europe/Madrid")
tz_utc = pytz.utc
now_utc = datetime.now(tz_utc)
now_madrid = now_utc.astimezone(tz_madrid)
print(now_madrid.strftime("%Y-%m-%d %H:%M %Z"))
# IMPORTANT: use localize(), NOT replace() with pytz
naive = datetime(2025, 6, 15, 10, 0, 0)
aware = tz_madrid.localize(naive) # ← CORRECT with pytz
print(aware.strftime("%Y-%m-%d %H:%M %Z%z"))
6. Unix timestamp ↔ datetime
from datetime import datetime, timezone
import time
# datetime → Unix timestamp
dt = datetime(2025, 6, 15, 14, 30, 0, tzinfo=timezone.utc)
ts = dt.timestamp()
print(f"Timestamp: {ts}") # 1750001400.0
# Unix timestamp → datetime
ts_now = time.time()
dt_from_ts = datetime.fromtimestamp(ts_now, tz=timezone.utc)
print(f"From timestamp: {dt_from_ts.isoformat()}")
# Millisecond timestamps (common in JavaScript/APIs)
ts_ms = int(ts_now * 1000)
dt_from_ms = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
print(f"From ms: {dt_from_ms}")
7. ISO 8601 and RFC 3339
from datetime import datetime, timezone
# Generate ISO 8601
now = datetime.now(timezone.utc)
print(now.isoformat()) # 2025-06-15T14:30:22.123456+00:00
print(now.strftime("%Y-%m-%dT%H:%M:%SZ")) # with trailing Z
# Parse ISO 8601 (Python 3.7+)
iso = "2025-06-15T14:30:00+02:00"
dt = datetime.fromisoformat(iso)
print(dt.utcoffset()) # 2:00:00
# Handle 'Z' suffix (Python < 3.11)
iso_z = "2025-06-15T14:30:00Z"
dt_z = datetime.fromisoformat(iso_z.replace("Z", "+00:00"))
print(dt_z)
8. Date ranges
from datetime import date, timedelta
import calendar
def date_range(start: date, end: date):
current = start
while current <= end:
yield current
current += timedelta(days=1)
# All Mondays in June 2025
for d in date_range(date(2025, 6, 1), date(2025, 6, 30)):
if d.weekday() == 0: # 0 = Monday
print(d.strftime("%A %Y-%m-%d"))
# First/last day of month
def first_of_month(year, month): return date(year, month, 1)
def last_of_month(year, month): return date(year, month, calendar.monthrange(year, month)[1])
print(first_of_month(2025, 2)) # 2025-02-01
print(last_of_month(2025, 2)) # 2025-02-28
9. Measuring execution time
import time
# perf_counter: high-resolution timer for benchmarks
start = time.perf_counter()
time.sleep(0.1)
end = time.perf_counter()
print(f"Duration: {end - start:.4f} s")
# Context manager
import contextlib
@contextlib.contextmanager
def timer(label="block"):
t0 = time.perf_counter()
yield
print(f"[{label}] {time.perf_counter() - t0:.4f} s")
with timer("processing"):
time.sleep(0.2)
10. dateutil: flexible parsing
# pip install python-dateutil
from dateutil import parser, relativedelta
from datetime import date
texts = ["June 15, 2025", "15 jun 2025", "2025/06/15", "Mon, 15 Jun 2025 14:30:00 +0200"]
for text in texts:
print(parser.parse(text).date())
# relativedelta: exact month/year offsets
today = date.today()
in_3_months = today + relativedelta.relativedelta(months=3)
in_1_year = today + relativedelta.relativedelta(years=1)
print(f"In 3 months: {in_3_months}")
print(f"In 1 year : {in_1_year}")
11. Best practices
- Always use aware datetimes in multi-timezone applications; naive datetimes cause subtle bugs.
- Store dates as UTC in the database; convert to local timezone only for display.
- Avoid
datetime.utcnow()— it's naive and misleading; usedatetime.now(timezone.utc). - pytz vs zoneinfo: prefer
zoneinfoon Python 3.9+; use pytz only for older compatibility. - ISO 8601 is the standard format for data exchange; use it in APIs and files.
time.perf_counter()for benchmarks,datetimefor business logic.
Module summary
| Module | Primary use | Install needed |
|---|---|---|
datetime |
Dates, times, arithmetic | No |
zoneinfo |
Timezones (Py 3.9+) | No |
calendar |
Calendars, days in month | No |
time |
Timestamps, benchmarks | No |
pytz |
Timezones (Py < 3.9) | Yes |
dateutil |
Flexible parsing | Yes |
Related conversions
Frequent conversions across the catalogue: