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.

A wizard’s clockwork workshop

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-day
  • time – when you only need hour:minute:second
  • datetime – the most common, combining date and time
  • timedelta – the difference or duration between two moments
  • timezone – 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 tzinfo object

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, use astimezone() (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 zone
  • astimezone(...): 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:


Previous series