2025, Oct 31 15:47

Ошибка replace при переводе времени Новой Зеландии в UTC в Python

Разбираем перевод летнего времени Новой Зеландии в UTC в Python с datetime и zoneinfo: почему replace не меняет tzinfo и как правильно выполнить конвертацию.

Преобразование временных меток между часовыми поясами в Python кажется простым, пока крошечная деталь не перевернёт итог с ног на голову. Частая ловушка — назначать часовой пояс объекту datetime через replace и думать, что он меняется «на месте». Это не так. Ниже — минимальный пример при переводе летнего времени Новой Зеландии (декабрь–февраль) в UTC, где NZ опережает UTC на 13 часов.

Постановка задачи

Цель: разобрать временную метку NZ, присвоить ей часовой пояс Новой Зеландии и перевести в UTC. Ожидаемое значение UTC для 2024-12-16T18:55:10 в период летнего времени NZ — 2024-12-16T05:55:10Z.

from zoneinfo import ZoneInfo
from datetime import datetime
# Строка времени в NZ
nz_stamp = '2024-12-16T18:55:10Z'
# Ожидаемое значение времени в UTC
utc_stamp = '2024-12-16T05:55:10Z'  # Летнее время NZ опережает UTC на 13 часов
# Разбор без привязки к часовому поясу
naive_local = datetime.strptime(nz_stamp, '%Y-%m-%dT%H:%M:%SZ')
# Попытка задать часовой пояс NZ
naive_local.replace(tzinfo=ZoneInfo('NZ'))
# Перевод в UTC
converted_utc = naive_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}')

Получаем:

Expected:  2024-12-16 05:55:10
Converted: 2024-12-16 17:55:10+00:00

Что пошло не так

Корневая проблема в том, что replace не изменяет datetime на месте. Он возвращает новый объект datetime. Поскольку результат не был присвоен переменной, объект остался без часового пояса, и дальнейшее преобразование дало неверный результат в UTC.

Исправление и рабочий пример

Сохраните результат replace перед вызовом astimezone. Этого достаточно, чтобы привести преобразование к ожидаемому значению в UTC.

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')
# Верно: сохранить возвращённый 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}')

Результат:

Expected:  2024-12-16 05:55:10
Converted: 2024-12-16 05:55:10+00:00

Минимальный вариант

Для наглядности — компактная версия без завершающей буквы Z во входной строке. Последовательность та же: разобрать как наивный, добавить часовой пояс NZ через replace и затем перевести в UTC.

import datetime as tmod
import zoneinfo as zmod
stamp = '2024-12-16T18:55:10'  # 'Z' не требуется
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}')

Вывод:

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

Почему эта деталь важна

Обработка временных рядов в период перехода на летнее время не прощает ошибок. Одного пропущенного присваивания при добавлении tzinfo достаточно, чтобы преобразования разошлись с фактическим локальным временем, а ошибка ушла дальше — в вычисления, логи и интеграции. Корректное понимание семантики replace гарантирует, что конвертации из летнего времени NZ в UTC будут согласованными и воспроизводимыми.

Выводы

При переводе между часовыми поясами с помощью datetime и zoneinfo в Python относитесь к replace как к чистой функции: всегда сохраняйте его результат перед astimezone. Для летних меток времени Новой Зеландии, опережающих UTC на 13 часов, эта небольшая правка стабильно даёт ожидаемый результат в UTC и удерживает ваши пайплайны в корректном состоянии.

Статья основана на вопросе на StackOverflow от IgorLopez и ответе Mark Tolonen.