Работа со временем в Python: tzinfo и pytz без магии и боли
Если вы хоть раз пытались работать с часовыми поясами, вы знаете: это ад из смещений, переходов на летнее время и странных правил разных стран. Но Python умеет в это неплохо — если правильно пользоваться tzinfo и библиотекой pytz.
---
### Наивные и осознанные datetime
По умолчанию datetime в Python не знает, в каком он часовом поясе.
from datetime import datetime
dt_naive = datetime(2024, 4, 5, 12, 0, 0)
print(dt_naive.tzinfo) # None
Это наивный объект — он не привязан ни к одному поясу. Любая арифметика и сравнения с другими датами могут быть некорректны, если вы смешиваете разные пояса.
---
### Интерфейс tzinfo
Класс tzinfo — это абстракция часового пояса. В теории вы можете написать свой класс, унаследованный от tzinfo, который определит:
- utcoffset() — смещение от UTC
- dst() — переход на летнее время
- tzname() — имя пояса
Но на практике руками это почти никогда не делают: слишком много нюансов. Поэтому используется pytz.
---
### Подключаем pytz
Устанавливаем:
pip install pytz
Простой пример: берём локальное время в Москве и переводим его в Нью-Йорк.
from datetime import datetime
import pytz
tz_moscow = pytz.timezone("Europe/Moscow")
tz_ny = pytz.timezone("America/New_York")
dt_naive = datetime(2024, 4, 5, 12, 0, 0)
dt_moscow = tz_moscow.localize(dt_naive)
dt_ny = dt_moscow.astimezone(tz_ny)
print(dt_moscow, dt_moscow.tzinfo) # 2024-04-05 12:00:00+03:00
print(dt_ny, dt_ny.tzinfo) # 2024-04-05 05:00:00-04:00
Ключевой момент — никогда не делать так:
# ПЛОХО
dt_wrong = datetime(2024, 4, 5, 12, 0, 0, tzinfo=tz_moscow)
С pytz это ломает обработку переходов на летнее время. Нужно именно localize().
---
### Храним в UTC, показываем пользователю в его поясе
Золотое правило: хранить время в UTC, показывать — в локальном часовом поясе.
from datetime import datetime
import pytz
utc = pytz.utc
tz_user = pytz.timezone("Asia/Tokyo")
# допустим, это пришло из БД как UTC
dt_stored = datetime(2024, 4, 5, 9, 0, 0, tzinfo=utc)
dt_user = dt_stored.astimezone(tz_user)
print("UTC:", dt_stored)
print("User time:", dt_user)
Так вы избегаете боли при переносе данных между сервером, БД и пользователями из разных стран.
---
### Подводные камни: неоднозначные и несуществующие времена
При переходах на зимнее/летнее время могут быть:
- Неоднозначные моменты (1:30 случается дважды)
- Несуществующие моменты (стрелка перепрыгивает через 2:00–3:00)
pytz умеет это обрабатывать через параметр is_dst:
from datetime import datetime
import pytz
tz = pytz.timezone("America/New_York")
dt_naive = datetime(2024, 11, 3, 1, 30, 0) # переход на зимнее время
dt_first = tz.localize(dt_naive, is_dst=True) # "летняя" 1:30
dt_second = tz.localize(dt_naive, is_dst=False) # "зимняя" 1:30
print(dt_first)
print(dt_second)
---
### Вывод
- Используйте tzinfo, но руками его не реализуйте — для реального мира берите pytz.
- Делайте localize() для привязки наивного datetime к поясу.
- Всегда храните время в UTC, а отображайте в нужной зоне через astimezone().
- Будьте осторожны с переходами на летнее/зимнее время — они реально ломают голову, но pytz знает все правила.