2025, Oct 27 11:00
Fixing Python datetime timezone conversion: capture replace(tzinfo) before astimezone for NZ summer to UTC
Learn why Python datetime.replace(tzinfo) doesn't mutate and how to correctly convert New Zealand DST timestamps to UTC using zoneinfo and astimezone.
Converting timestamps between time zones in Python looks straightforward until a tiny detail derails the entire result. A common trap is assigning a time zone to a datetime using replace and assuming the object changes in place. It doesn’t. Below is a minimal case from converting New Zealand summer time (Dec–Feb) to UTC, where NZ time is 13 hours ahead of UTC.
Problem setup
The intent is to parse an NZ timestamp, attach the NZ time zone, and convert it to UTC. The expected UTC value for 2024-12-16T18:55:10 in NZ summer is 2024-12-16T05:55:10Z.
from zoneinfo import ZoneInfo
from datetime import datetime
# NZ time string
nz_stamp = '2024-12-16T18:55:10Z'
# Expected UTC time string
utc_stamp = '2024-12-16T05:55:10Z'  # NZ summertime is 13 hours ahead of UTC
# Parse as timezone-unaware
naive_local = datetime.strptime(nz_stamp, '%Y-%m-%dT%H:%M:%SZ')
# Attempt to set NZ timezone
naive_local.replace(tzinfo=ZoneInfo('NZ'))
# Convert to UTC
converted_utc = naive_local.astimezone(ZoneInfo('UTC'))
# Prepare expected value (still printed naive here)
expected_utc = datetime.strptime(utc_stamp, '%Y-%m-%dT%H:%M:%SZ')
expected_utc.replace(tzinfo=ZoneInfo('UTC'))
print(f'Expected:  {expected_utc}')
print(f'Converted: {converted_utc}')
This yields:
Expected:  2024-12-16 05:55:10
Converted: 2024-12-16 17:55:10+00:00
What went wrong
The core issue is that replace does not modify the datetime in place. It returns a new datetime object. Because the returned value wasn’t captured, the object stayed timezone-unaware and the downstream conversion produced the wrong UTC result.
Fix and working example
Assign the result of replace before calling astimezone. That alone aligns the conversion with the expected UTC timestamp.
from zoneinfo import ZoneInfo
from datetime import datetime
nz_stamp = '2024-12-16T18:55:10Z'
utc_stamp = '2024-12-16T05:55:10Z'
naive_local = datetime.strptime(nz_stamp, '%Y-%m-%dT%H:%M:%SZ')
# Correct: capture the returned aware datetime
aware_local = naive_local.replace(tzinfo=ZoneInfo('NZ'))
converted_utc = aware_local.astimezone(ZoneInfo('UTC'))
expected_utc = datetime.strptime(utc_stamp, '%Y-%m-%dT%H:%M:%SZ')
expected_utc.replace(tzinfo=ZoneInfo('UTC'))
print(f'Expected:  {expected_utc}')
print(f'Converted: {converted_utc}')
Result:
Expected:  2024-12-16 05:55:10
Converted: 2024-12-16 05:55:10+00:00
Minimal variant
For clarity, here’s a compact version without the trailing Z in the input. The sequence is the same: parse as unaware, attach NZ tz using replace, then convert to UTC.
import datetime as tmod
import zoneinfo as zmod
stamp = '2024-12-16T18:55:10'  # 'Z' not needed
plain_dt = tmod.datetime.strptime(stamp, '%Y-%m-%dT%H:%M:%S')
nz_dt = plain_dt.replace(tzinfo=zmod.ZoneInfo('NZ'))
utc_dt = nz_dt.astimezone(tmod.UTC)
print(f'{plain_dt = !s}')
print(f'{nz_dt    = !s}')
print(f'{utc_dt   = !s}')
Output:
plain_dt = 2024-12-16 18:55:10
nz_dt    = 2024-12-16 18:55:10+13:00
utc_dt   = 2024-12-16 05:55:10+00:00
Why this detail matters
Time-series processing during daylight saving periods is unforgiving. A single missed assignment when attaching tzinfo makes conversions diverge from the true wall time, and that propagates into downstream computations, logs, and integrations. Getting the replace semantics right ensures conversions from NZ summer time to UTC are consistent and reproducible.
Takeaways
When converting between time zones with Python’s datetime and zoneinfo, treat replace as a pure function: always capture its return value before calling astimezone. For New Zealand summer timestamps that are 13 hours ahead of UTC, this small correction reliably produces the expected UTC result and keeps your pipelines aligned.
The article is based on a question from StackOverflow by IgorLopez and an answer by Mark Tolonen.