Mastering Time with Python’s Standard Library: The Complete Guide to datetime
Series 03 – From date/time arithmetic to time zones and format conversion, all in one go
Time isn’t just a “string.” Dates roll over, months change, daylight saving kicks in, and local conventions differ. When working with dates and times, it’s essential to separate the display string from the underlying time value for calculations and to incorporate time-zone handling into your workflow.

In this post we’ll focus on the datetime module and cover:
- Creating the current date and time
- Calculating durations (
timedelta) - Formatting and parsing strings (
strftime,strptime) - Handling time zones (
timezone,zoneinfo) - Common pitfalls and reliable patterns
1. What does datetime provide?
datetime contains several types that look similar but serve different purposes.
date– when you only need year-month-daytime– when you only need hour:minute:seconddatetime– the most common, combining date and timetimedelta– the difference or duration between two momentstimezone– a fixed-offset time zone (e.g., UTC+9)
Python 3.9+ added the standard library zoneinfo, making it easier to work with regional time zones like Asia/Tokyo.
2. Getting “now”: naive vs aware
2.1 What are naive and aware?
datetime objects fall into two categories:
- Naive datetime – no time-zone information
- Aware datetime – carries a
tzinfoobject
Mixing the two in calculations or comparisons can raise errors or produce unexpected results.
2.2 Recommended default: start with UTC-aware
Normalizing storage and calculations to UTC keeps things tidy.
from datetime import datetime, timezone
utc_now = datetime.now(timezone.utc) # aware (UTC)
print(utc_now)
When you need a local time, convert at the display stage.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
utc_now = datetime.now(timezone.utc)
tokyo_now = utc_now.astimezone(ZoneInfo("Asia/Tokyo"))
print(utc_now)
print(tokyo_now)
3. Date/time arithmetic: timedelta at the core
3.1 Adding and subtracting
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
print(now + timedelta(days=3))
print(now - timedelta(hours=2))
3.2 Difference between two moments
from datetime import datetime, timezone
a = datetime(2026, 1, 1, tzinfo=timezone.utc)
b = datetime(2026, 1, 10, tzinfo=timezone.utc)
delta = b - a
print(delta.days) # 9
print(delta.total_seconds()) # 777600.0
3.3 Expressing a “period” with timedelta
timedelta works in days, seconds, and microseconds. Roughly “one month later” can be approximated with timedelta(days=30), but calendar-based logic (e.g., the same day next month) requires tools like dateutil outside the standard library.
4. Formatting and parsing: strftime / strptime
4.1 datetime → string (strftime)
from datetime import datetime, timezone
dt = datetime(2026, 1, 30, 14, 5, 0, tzinfo=timezone.utc)
print(dt.strftime("%Y-%m-%d %H:%M:%S %z"))
Common format patterns:
%Y-%m-%d: 2026-01-30%H:%M:%S: 14:05:00%z: +0000 (UTC offset)
4.2 string → datetime (strptime)
from datetime import datetime
s = "2026-01-30 14:05:00"
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
print(dt)
The resulting dt is naive. To attach a time zone, use replace(tzinfo=...).
from datetime import datetime, timezone
s = "2026-01-30 14:05:00"
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
print(dt)
replace(tzinfo=…)labels the time without converting it. For conversion, useastimezone()(see next section).
5. Time zones: distinguishing timezone and zoneinfo
5.1 Fixed offset – use timezone
from datetime import datetime, timezone, timedelta
kst_fixed = timezone(timedelta(hours=9))
dt = datetime(2026, 1, 30, 12, 0, tzinfo=kst_fixed)
print(dt)
5.2 Regional zone – use zoneinfo
Zones that observe daylight saving need zoneinfo.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
utc_now = datetime.now(timezone.utc)
ny_now = utc_now.astimezone(ZoneInfo("America/New_York"))
print(ny_now)
5.3 replace(tzinfo=…) vs astimezone(...)
replace(tzinfo=…): keeps the clock value, just tags a zoneastimezone(...): converts the same instant to another zone’s clock
Knowing this difference helps prevent many time-zone bugs.
6. Common pitfalls
6.1 Mixing naive/aware
- Keep internal calculations in aware (UTC) for stability.
- Normalize external input to a single zone as soon as it arrives.
6.2 “Local time” varies by environment
datetime.now() follows the host’s local settings. In containers or servers, the local time might be UTC or something else. Prefer datetime.now(timezone.utc) to avoid surprises.
6.3 String parsing format mismatches
strptime fails if the format differs by even one character. If you expect multiple formats, pre-process or try candidates sequentially. Ideally, standardize the input format.
7. Three handy patterns
7.1 Store as ISO 8601
from datetime import datetime, timezone
dt = datetime.now(timezone.utc)
print(dt.isoformat()) # e.g., 2026-01-30T05:12:34.567890+00:00
7.2 Filenames with today’s date
from datetime import datetime
stamp = datetime.now().strftime("%Y%m%d")
filename = f"report_{stamp}.json"
print(filename)
7.3 Time remaining until a target
from datetime import datetime, timezone
target = datetime(2026, 2, 1, 0, 0, tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
remaining = target - now
print(remaining)
print(remaining.total_seconds())
8. Wrap-up
datetime moves you beyond simple string creation to a full-fledged tool for time calculations. Coupled with timezone and zoneinfo, you can write code that behaves consistently across environments.
Next time, we’ll dedicate a separate post to random, covering random generation, sampling, shuffling, seeding, and secure randomness with secrets.
Related posts:
- Using datetime and timezone correctly in Django
- Django’s time-management magic – the complete guide to
django.utils.timezone
Previous series