Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 14 из 20 Начальный 25 мин 140 XP

Часовые пояса: timezone и offset

Naive-дата не знает своего пояса, aware — знает: tzinfo хранит смещение от UTC. Один момент в двух поясах — разные цифры на часах и одно и то же время.

Редакция Питоники

До сих пор мы печатали даты без смещений: 2026-10-21 14:30 — и молчали о том, где эти 14:30 наступают. Между Москвой и Лондоном разница в три часа, поэтому 14:30 без пояса — не время, а загадка. Разбираемся, как datetime хранит пояс и что меняется, когда он известен.

Naive и aware

дата без пояса и с поясом
from datetime import datetime, timezone

naive = datetime(2026, 10, 21, 11, 30)
aware = datetime(2026, 10, 21, 11, 30, tzinfo=timezone.utc)
print(naive)
print(aware)
print(aware.utcoffset())
Вывод
2026-10-21 11:30:00
2026-10-21 11:30:00+00:00
0:00:00

Объект без пояса называется naive — наивный: цифры у него есть, но он не знает, какому поясу они принадлежат. Объект с tzinfo — aware: он знает своё смещение от UTC и печатает его плюсом — +00:00 у timezone.utc. Метод utcoffset() возвращает смещение объектом timedelta: для UTC это ноль часов. Все даты из предыдущих уроков были naive — теперь понятно, чего им не хватало.

Своё смещение: timezone(timedelta(...))

московское смещение
from datetime import datetime, timedelta, timezone

msk = timezone(timedelta(hours=3))
d = datetime(2026, 10, 21, 14, 30, tzinfo=msk)
print(d)
print(d.utcoffset())
Вывод
2026-10-21 14:30:00+03:00
3:00:00

Класс timezone принимает фиксированное смещение объектом timedelta — три часа для Москвы. В repr смещение видно честно: datetime.timezone(datetime.timedelta(seconds=10800)) — три часа, переведённые в 10 800 секунд. Для фиксированных смещений больше ничего не нужно: база зон zoneinfo пригодится для исторических правил и переходов на летнее время, а здесь достаточно честного +03:00.

Один момент — двое часов

UTC и Москва
from datetime import datetime, timedelta, timezone

moment = datetime(2026, 10, 21, 11, 30, tzinfo=timezone.utc)
msk = moment.astimezone(timezone(timedelta(hours=3)))
print(moment)
print(msk)
print(moment == msk)
Вывод
2026-10-21 11:30:00+00:00
2026-10-21 14:30:00+03:00
True

Метод astimezone переводит тот же момент в другой пояс: 11:30 по UTC — это 14:30 по московскому. Часы показывают разное, но это одно и то же время: сравнение aware-дат сравнивает моменты, а не цифры. Поэтому moment == msk вернуло True. Тот же принцип работал и в unix-времени из прошлого урока: число одно — часов много.

вычитание aware-дат
from datetime import datetime, timedelta, timezone

utc = datetime(2026, 10, 21, 11, 30, tzinfo=timezone.utc)
msk = datetime(2026, 10, 21, 14, 30, tzinfo=timezone(timedelta(hours=3)))
print(utc - msk)
print(msk - utc)
Вывод
0:00:00
0:00:00

Откуда берётся смещение +03:00

Запись +03:00 в конце даты — это не украшение и не подпись. Это разница между местным временем и UTC в часах и минутах, и она говорит о двух вещах сразу: в какой зоне живут часы и насколько далеко эта зона от Гринвича. Москва зимой и летом 2026 года показывает +03:00, Лондон зимой показывает +00:00, а летом перескакивает на +01:00. Число в записи меняется, а страна та же — значит, это не навсегда прибитая характеристика.

смещение как объект timedelta
from datetime import datetime, timedelta, timezone

msk = timezone(timedelta(hours=3))
d = datetime(2026, 10, 21, 14, 30, tzinfo=msk)

print(d.utcoffset())
print(d.utcoffset().total_seconds())
print(d.tzname())
Вывод
3:00:00
10800.0
UTC+03:00

Метод utcoffset() отдаёт смещение объектом timedelta — в уроке это 3:00:00, то есть 10 800 секунд. Безымянный timezone(timedelta(hours=3)) при этом не остаётся анонимным: tzname() для него возвращает строку UTC+03:00, которую Python собирает из самого смещения. Если нужен осмысленный ярлык в логах или письмах, задайте имя вторым аргументом: timezone(timedelta(hours=3), 'MSK') даст в repr datetime.timezone(datetime.timedelta(seconds=10800), 'MSK') и напечатает MSK вместо UTC+03:00.

пояс с именем
from datetime import datetime, timedelta, timezone

msk = timezone(timedelta(hours=3), 'MSK')
d = datetime(2026, 10, 21, 14, 30, tzinfo=msk)

print(d)
print(d.tzname())
Вывод
2026-10-21 14:30:00+03:00
MSK

Один момент — три записи

Момент, который в Москве 14:30, в UTC записывается как 11:30, а в Нью-Йорке — как 07:30 или 08:30 в зависимости от сезона. Три записи — один момент, и все три равны друг другу при сравнении. Это и есть смысл aware-дат: цифры меняются, суть остаётся.

три пояса, один момент
from datetime import datetime, timedelta, timezone

moment = datetime(2026, 10, 21, 14, 30, tzinfo=timezone(timedelta(hours=3)))

utc = moment.astimezone(timezone.utc)
ny = moment.astimezone(timezone(timedelta(hours=-4)))

print(moment)
print(utc)
print(ny)
print(moment == utc == ny)
Вывод
2026-10-21 14:30:00+03:00
2026-10-21 11:30:00+00:00
2026-10-21 07:30:00-04:00
True

astimezone не меняет момент, а меняет его запись: 14:30+03:00 превратилось в 11:30+00:00 — это тот же самый момент, пересчитанный по-другому. Цепочка moment == utc == ny вернула True: Python сравнивает aware-даты по абсолютному моменту, а не по напечатанным цифрам. Отсюда практическое следствие: если две записи в базе выглядят по-разному, но равны при сравнении, это нормальные данные, а не дубль.

replace против astimezone
from datetime import datetime, timedelta, timezone

utc = datetime(2026, 10, 21, 11, 30, tzinfo=timezone.utc)

wrong = utc.replace(tzinfo=timezone(timedelta(hours=3)))
right = utc.astimezone(timezone(timedelta(hours=3)))

print(wrong)
print(right)
print(wrong == utc)
print(right == utc)
Вывод
2026-10-21 11:30:00+03:00
2026-10-21 14:30:00+03:00
False
True

Два объекта с одинаковыми цифрами на часах, но разными моментами: 11:30+03:00 — это 08:30 UTC, а 14:30+03:00 — это 11:30 UTC. Один из них тот момент, который был, другой — на два с половиной часа раньше. Разница между этими двумя ошибками стоила нам сдвига графика отчёта на полдня: в базе лежали локальные часы ночной смены, а приводили мы их к UTC как будто они уже были UTC.

Сравнение и сортировка

С aware-датами работают все обычные приёмы из урока про сравнение дат: меньше, больше, равно, сортировка списка. Разница в том, что сравнение теперь идёт по абсолютному моменту, а не по напечатанным цифрам, — и это единственное, что нужно знать, чтобы не запутаться.

сортировка по моменту, а не по часам
from datetime import datetime, timedelta, timezone

msk = timezone(timedelta(hours=3))
utc = timezone.utc

events = [
    datetime(2026, 10, 21, 14, 30, tzinfo=msk),
    datetime(2026, 10, 21, 10, 30, tzinfo=utc),
    datetime(2026, 10, 21, 12, 30, tzinfo=utc),
]

for d in sorted(events):
    print(d)
Вывод
2026-10-21 10:30:00+00:00
2026-10-21 14:30:00+03:00
2026-10-21 12:30:00+00:00

Напечатанные часы идут 10:30, 14:30, 12:30 — по цифрам это выглядит как ошибка. Но 14:30+03:00 это те же 11:30 UTC, то есть между 10:30 и 12:30. Сортировка посмотрела на момент, а не на цифры, и выстроила события правильно. Если бы мы сравнивали эти даты как строки, порядок был бы неверным — ровно тот случай, о котором пойдёт речь в уроке про логи.

Практика: где хранить и где переводить

Мы договорились о трёх вещах, и они снимают почти все проблемы. В базе лежит UTC — приведённый к нему момент с поясом, а не локальное время машины, которая писала строку. В логах пишется UTC с явной отметкой, и разбирается он обратно через fromisoformat из урока про ISO — там формат уже отформатирован, разбирать нечего. А перевод в местное время происходит в самом конце, на границе интерфейса: показать пользователю его собственное «сейчас».

храним UTC, показываем местное
from datetime import datetime, timedelta, timezone

stored = datetime(2026, 10, 21, 11, 30, tzinfo=timezone.utc)

for offset in (0, 3, -4):
    zone = timezone(timedelta(hours=offset))
    print(stored.astimezone(zone).strftime('%d.%m.%Y %H:%M %z'))
Вывод
21.10.2026 11:30 +0000
21.10.2026 14:30 +0300
21.10.2026 07:30 -0400

Одно и то же сохранённое значение, три разных показания — и все три верны, потому что показывают разным людям разное местное время. Код %z в формате добавляет смещение текстом: +0000, +0300, -0400. Кто-то из разработчиков обязательно спросит, а зачем хранить UTC, если пользователь всё равно видит локальное время: потому что UTC — единственное значение, которое не переврачивается при переезде сервера, при смене часового пояса и при объединении данных из разных источников.

ОперацияNaiveAware
print(d)2026-10-21 11:30:002026-10-21 11:30:00+00:00
сравнение с другой aware-датойTypeErrorсравниваются моменты
вычитаниесчитает «часы на часах»считает реальную разницу
timestamp()зависит от пояса машиныдетерминирован
astimezone()считает локальным временем машиныпереводит тот же момент

Таблица показывает главное: naive-дата не ломается только до первого сравнения с aware или первого timestamp(). До этого она выглядит рабочей, а потом внезапно начинает зависеть от машины. Если разобрать все пять строк, видно, что за каждой строкой стоит один и тот же вопрос — знает ли объект свой пояс. Как только знает, всё остальное работает само.

Что дальше

С поясами разобрались: naive против aware, astimezone, offset и имена зон. Дальше — модуль calendar: он работает с календарём как таковым, без часовых поясов вообще, зато умеет нарисовать месяц целиком и ответить, каким днём недели была дата. А если захочется перейти от дат к таблицам и строкам выгрузки — урок про даты в Pandas.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

from datetime import datetime, timezone

d = datetime(2026, 10, 21, 9, 0, tzinfo=timezone.utc)
print(d)
from datetime import datetime, timedelta, timezone

utc = datetime(2026, 10, 21, 8, 0, tzinfo=timezone.utc)
msk = utc.astimezone(timezone(timedelta(hours=3)))
print(msk)
from datetime import datetime, timedelta, timezone

a = datetime(2026, 10, 21, 14, 0, tzinfo=timezone(timedelta(hours=3)))
b = datetime(2026, 10, 21, 14, 0, tzinfo=timezone.utc)
print(b - a)
from datetime import datetime, timedelta, timezone

moment = datetime(2026, 10, 21, 14, 30, tzinfo=timezone(timedelta(hours=3)))
ny = moment.astimezone(timezone(timedelta(hours=-4)))
print(ny)
from datetime import datetime, timedelta, timezone

utc = datetime(2026, 10, 21, 11, 30, tzinfo=timezone.utc)
wrong = utc.replace(tzinfo=timezone(timedelta(hours=3)))
print(wrong == utc)
print(wrong.astimezone(timezone.utc))
Проверь себя
0 / 9

1. Чем aware-datetime отличается от naive?

2. Как задать пояс UTC+3?

3. 11:30 UTC и 14:30 MSK — один момент. Что вернёт оператор == между ними?

4. Что вернёт вычитание двух aware-дат из разных поясов?

5. Что произойдёт при вычитании naive из aware?

6. Чем astimezone отличается от replace(tzinfo=...)?

7. Почему фиксированный timezone(timedelta(hours=3)) не подходит для Лондона?

8. Что вернёт d.tzname() для timezone(timedelta(hours=3)) без имени?

9. Момент 14:30+03:00 и момент 12:30+00:00. Что раньше?

Карточки терминов
Запомнено: 0 / 11
Практика

Возьмите aware-дату berlin = datetime(2026, 6, 15, 12, 0, tzinfo=timezone(timedelta(hours=2))). Выведите её. Переведите момент в UTC через astimezone и выведите результат. Выведите разницу berlin - utc.

practice.py
Вопросы и ответы по уроку

Чем отличаются naive и aware даты в Python?

Naive-дата создана без tzinfo и не знает своего пояса — print показывает просто 2026-10-21 11:30:00. Aware-дата хранит смещение и печатает его плюсом: 2026-10-21 11:30:00+00:00. Для всего, что переживёт один компьютер — логов, API, баз, — используйте aware-даты.

Как задать московское время в Python?

Фиксированным смещением: msk = timezone(timedelta(hours=3)), затем datetime(2026, 10, 21, 14, 30, tzinfo=msk) — вы получите aware-дату с отметкой +03:00. Для фиксированных смещений база зон zoneinfo не нужна: достаточно timezone и timedelta.

Что делает astimezone?

Переводит тот же момент времени в другой пояс: 11:30+00:00 превращается в 14:30+03:00. Момент не меняется — меняются цифры на часах, поэтому исходная и переведённая даты равны при сравнении ==. В unix-времени это то же самое число секунд.

Урок 13: timestamp

Почему нельзя сравнивать naive и aware даты?

Наивная дата не знает пояса, поэтому Python не может свести её с aware-датой к одному моменту и выбрасывает TypeError: can't compare offset-naive and offset-aware datetimes (и аналогичный при вычитании). Правило одно: все даты в проекте — aware, а пояс задаётся timezone.utc или timezone(timedelta(hours=...)).

Как привести дату к московскому времени в Python?

Сначала сделайте дату aware, потом переведите её: msk = timezone(timedelta(hours=3)), local = moment.astimezone(msk). astimezone сохраняет момент и меняет только запись — 11:30+00:00 станет 14:30+03:00. Не используйте для этого replace(tzinfo=...): он сдвигает сам момент, и 11:30+00:00 превратится в 08:30 UTC.

Урок 14: часовые пояса

Что такое zoneinfo и зачем он, если есть timezone?

timezone(timedelta(hours=3)) — это фиксированное смещение, вечное UTC+3 без переходов. zoneinfo из стандартной библиотеки знает настоящие зоны по ключам вида Europe/Moscow и учитывает переход на летнее время и смену правил в стране. Плата — база данных часовых поясов: в браузерной песочнице её может не быть, и тогда приходится обходиться фиксированным смещением.

Как узнать смещение часового пояса у datetime?

Методом d.utcoffset() — он возвращает timedelta, для +03:00 это 3:00:00, а total_seconds() даёт 10800.0. У naive-дат без пояса utcoffset() возвращает None, и это удобный способ проверить, aware ли объект, не ловя исключение.

Почему в базе лучше хранить UTC, а не локальное время?

Потому что UTC — единственное значение, которое не переврачивается: при переезде сервера в другой часовой пояс, при смене настроек машины и при объединении данных из разных источников оно остаётся тем же самым моментом. Локальное время приходится переводить обратно, причём не зная, в каком поясе оно было записано. В базе держим UTC, а перевод в местное делаем на границе интерфейса через astimezone.

Понравился урок? Сошлитесь на него

«Часы показывают разное, но это одно и то же время: сравнение aware-дат сравнивает моменты, а не цифры.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.