Одно местное время соответствует двум событиям: zoneinfo и fold
В журнале доставки два события имеют одинаковые дату и время в Нью-Йорке. Они произошли с интервалом в час, но после загрузки в Python сравнение локальных datetime считает их равными. Во время осеннего перевода часов это возможно без ошибки источника.
Часы на стене не всегда задают момент
Когда смещение часового пояса уменьшается, часть местных значений повторяется. Запись с датой, часами, минутами и названием зоны может указывать на два разных момента. Для разового события удобнее принимать ISO-строку с числовым смещением либо UTC. Зона нужна отдельно, когда надо показывать время пользователю или рассчитывать будущие события по местному календарю.
У datetime есть атрибут fold. Для неоднозначного значения fold=0 выбирает смещение до перехода, fold=1 — после него. Это дополнительное знание должно прийти из источника либо явного решения пользователя. Автоматически выбрать второй вариант по порядку строк журнала нельзя: доставка событий может менять порядок, а часть записей может отсутствовать.
ZoneInfo использует базу IANA. В среде без системных данных о зонах потребуется пакет tzdata; это особенно актуально для Windows и минимальных образов. Если имя зоны не найдено, исправьте зависимости окружения. Замена зоны на фиксированное смещение уберёт исторические и будущие правила перевода и изменит смысл календарных вычислений.
Сравнивать моменты после перевода в UTC
Пример берёт известный переход 3 ноября 2024 года. Два локальных значения показывают 01:30, но их UTC-представления отличаются на час. Для вычисления продолжительности переводим оба в UTC. Это также обходит особенность сравнения datetime с одним и тем же tzinfo: при таком сравнении fold и смещения не используются для различения одинакового локального показания.
При чтении строки с числовым смещением Python уже получает определённый момент. После astimezone в нужную ZoneInfo устанавливается подходящий fold. Таким образом, на границе системы можно сохранить точность события, а в интерфейсе вернуть местное представление. Для сортировки, ключа дедупликации по времени и длительности используйте нормализованный момент вместе с идентификатором события.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")
first = datetime(2024, 11, 3, 1, 30, tzinfo=ny, fold=0)
second = datetime(2024, 11, 3, 1, 30, tzinfo=ny, fold=1)
first_utc = first.astimezone(timezone.utc)
second_utc = second.astimezone(timezone.utc)
assert first.isoformat() == "2024-11-03T01:30:00-04:00"
assert second.isoformat() == "2024-11-03T01:30:00-05:00"
assert (second_utc - first_utc).total_seconds() == 3600
assert first == second
parsed = datetime.fromisoformat("2024-11-03T01:30:00-05:00")
localized = parsed.astimezone(ny)
assert localized.fold == 1
assert localized.astimezone(timezone.utc) == second_utc
print(first_utc.isoformat(), second_utc.isoformat())Договориться о входном формате
Если API принимает местное время без смещения, добавьте правило обработки неоднозначности. Например, потребуйте смещение при повторяющемся часе или отдельное подтверждение выбранного варианта. Не оставляйте fold=0 незаметным бизнес-решением только потому, что это значение по умолчанию. В сообщении об ошибке покажите два доступных смещения, чтобы клиент мог отправить определённый момент.
Весной существует другая проблема: некоторые локальные показания вообще не наступают. Простое присоединение tzinfo к naive datetime не выполняет полноценную проверку календарного ввода. Для пользовательского планировщика нужна отдельная валидация, в том числе обратное преобразование через UTC и политика для отсутствующего времени. Это следующий сценарий тестирования; fold сам по себе его не решает.
Храните UTC-момент и исходную зону, если она нужна для дальнейшего отображения. Для расписания «каждый день в девять» храните локальное правило и зону: прибавление фиксированных суток к UTC-моменту не сохраняет девять утра после перехода. Разделение события и календарного правила делает модель понятной и помогает выбрать правильные тесты для каждого из них.
При диагностике журналов выводите вместе с местным временем числовое смещение и нормализованное UTC-значение. Одних названий вроде EST или CET недостаточно для устойчивого формата обмена. Если старая таблица содержит только местные строки без зоны, восстановить точный момент иногда невозможно без дополнительных сведений: порядка событий, смещения из соседнего источника или отдельного идентификатора операции. Не исправляйте такие записи массовой установкой fold наугад. Сначала выделите неоднозначный интервал и договоритесь, как отмечать неизвестность в восстановленных данных. Для новых событий измените формат записи на границе системы. Это небольшой шаг, который избавляет последующие аналитические и серверные расчёты от необходимости каждый раз угадывать смысл исторической строки по косвенным признакам и неполному контексту.
Следующий шаг в практике
Начните с бесплатного урока о тестах pytest: превратите две временные записи в воспроизводимый регрессионный тест. Программа продвинутого Python продолжает работу с моделями и тестированием. Для системного знакомства с продвинутой стандартной библиотекой откройте курс «Python для профессионалов».
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
