timestamp: unix-время и эпоха 1970
Unix-время — число секунд с полуночи 1 января 1970 года по UTC. Считаем timestamp у aware-дат, переводим число обратно через fromtimestamp — и всегда указываем пояс.
Редакция Питоники
В логах, ответах API и базах данных время часто лежит одним числом: 1792593045. Это unix-время — количество секунд, прошедших с условного нуля отсчёта. Python умеет переводить даты в такие числа и обратно, но у этого моста есть подводный камень с часовыми поясами — разберём обе стороны.
Эпоха: полночь 1 января 1970 по UTC
from datetime import datetime, timezone
epoch = datetime.fromtimestamp(0, tz=timezone.utc)
print(epoch)
print(repr(epoch))
1970-01-01 00:00:00+00:00 datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
Ноль секунд после эпохи — это полночь 1 января 1970 года по UTC: с этого момента мир договорился считать unix-время. fromtimestamp переводит число в datetime, и здесь принципиален аргумент tz=timezone.utc: само число ничего не знает о поясах, поэтому пояс нужно показать явно. Только с указанным tz результат одинаков на любой машине — от ноутбука в Москве до сервера в Амстердаме.
from datetime import datetime, timezone
d = datetime(2026, 10, 21, 14, 30, 45, tzinfo=timezone.utc)
ts = d.timestamp()
print(ts)
print(round(ts))
1792593045.0 1792593045
Метод timestamp() у aware-объекта возвращает, сколько секунд прошло с эпохи до его момента: у 14:30:45 UTC 21 октября 2026 года получилось 1 792 593 045. Возвращается float, потому что datetime хранит микросекунды и дробная часть возможна; round(ts) даёт целое число. У дат без микросекунд, как здесь, дробной части нет.
Один момент — одно число
from datetime import datetime, timedelta, timezone
utc = datetime(2026, 10, 21, 11, 30, 45, tzinfo=timezone.utc)
msk = datetime(2026, 10, 21, 14, 30, 45, tzinfo=timezone(timedelta(hours=3)))
print(utc.timestamp())
print(msk.timestamp())
print(utc.timestamp() == msk.timestamp())
1792582245.0 1792582245.0 True
11:30 по UTC и 14:30 по московскому времени — один и тот же момент, и timestamp это подтверждает: числа совпали. В этом смысл unix-времени. Число — сам момент, а пояс лишь решает, какими цифрами момент покажется на часах. Именно поэтому unix-время удобно хранить и сравнивать: одна шкала для всей планеты.
from datetime import datetime, timezone
d = datetime(2026, 10, 21, 14, 30, 45, tzinfo=timezone.utc)
back = datetime.fromtimestamp(d.timestamp(), tz=timezone.utc)
print(back)
print(back == d)
2026-10-21 14:30:45+00:00 True
Круг замкнулся: datetime превратился в число и вернулся обратно равным себе — при условии, что обе стороны помнят про tz. Осталось разобраться, что такое пояса и откуда берётся запись +03:00: этим займётся следующий урок.
Миллисекунды: откуда берётся умножение на 1000
В реальных системах unix-время почти никогда не доходит до вас в секундах. JavaScript отдаёт Date.now() в миллисекундах, PostgreSQL возвращает timestamp с микросекундами, Kafka кладёт в ключ целое число миллисекунд. Python со своей стороны отдаёт float секунд — и разница в множителе становится самой частой ошибкой при разборе внешних данных. Хуже всего, что ошибка не бросается: число получается в тысячу раз меньше, чем должно, и выглядит как правдоподобная дата 1970 года.
from datetime import datetime, timezone
d = datetime(2026, 10, 21, 14, 30, 45, tzinfo=timezone.utc)
ts = d.timestamp()
print(ts)
print(int(ts * 1000))
print(int(ts * 1_000_000))
1792593045.0 1792593045000 1792593045000000
Три числа — один и тот же момент в трёх единицах. Множители разные, а риск общий: float хранит примерно 15-16 значащих цифр, и при умножении на миллион запас кончается гораздо раньше, чем кажется. Отсюда правило: чем крупнее единица, тем ближе к границе точности вы приближаетесь, а для целых чисел в базе переходите на int — точные целые Python хранит без потерь.
from datetime import datetime, timezone
half = datetime(2026, 10, 21, 14, 30, 45, 500000, tzinfo=timezone.utc)
print(half.timestamp())
print(int(half.timestamp()))
print(round(half.timestamp()))
before = datetime(1969, 12, 31, 23, 59, 59, 500000, tzinfo=timezone.utc)
print(int(before.timestamp()))
1792593045.5 1792593045 1792593046 0
int отбрасывает дробную часть к нулю, а round округляет по правилу банка — к ближайшему чётному. На половине секунды они расходятся на целую секунду: 1792593045.5 превращается то в 1792593045, то в 1792593046. Если вы переводите timestamp в целые миллисекунды для метрики или ключа, выбирайте одну функцию и не меняйте её между слоями: неделю мы ловили метрику, которая на ровно полусекундных границах считала событие то в одном бакете, то в соседнем.
Где кончается точность float
Двоичная плавающая точка хранит 53 значащих бита. Это примерно 9 007 199 254 740 992 целых — тот предел, дальше которого соседние целые float не различает и округляет к ближайшему чётному. Секунды unix-времени живут внутри этого предела с колоссальным запасом: 2**53 секунд — это около 285 миллионов лет. Микросекунды в том же запасе не помещаются.
# 53 значащих бита: целые дальше этой отметки float не различает
print(2 ** 53)
print(2 ** 53 // 1000)
print(2 ** 53 // 1000 // 1000)
9007199254740992 9007199254740 9007199254
Переведите вторую строку в годы — примерно 285 лет от эпохи, нормальный запас. Третья строка — микросекунды, и там всего около двух с половиной часов. Практический вывод: unix-время в микросекундах внутри float начинает врать задолго до 2262 года, а не в 2262-м. Мы перестали хранить микросекунды в float и перешли на int — там переполнение можно хотя бы заметить, потому что оно выдаёт себя круглым числом.
2038: конец 32-битного счётчика
from datetime import datetime, timezone
limit = 2 ** 31 - 1
print(limit)
print(datetime.fromtimestamp(limit, tz=timezone.utc))
2147483647 2038-01-19 03:14:07+00:00
2147483647 секунд — это 19 января 2038 года, 03:14:07 UTC. Ровно в эту секунду 32-битный знаковый int переполняется и уезжает в отрицательные значения, то есть в начало шкалы, к 1970 году. Если timestamp лежит в колонке INT, а не BIGINT, или в 32-битном поле протокола, через двенадцать лет значения начнут прыгать назад на полвека. Мы нашли такое в чужом экспорте: счётчик событий стоял в INT, и вместо номера события мы получали дату 1970 года.
Сортировка: числам не нужен datetime
Если события уже лежат числами, сортировать их можно вообще без всяких дат: обычный sorted() по списку целых работает и быстрее, и без риска, что кто-то поставит datetime в один контейнер, а строку — в другой. Обратное преобразование делают уже по одному, когда значение понадобилось показать.
from datetime import datetime, timezone
events = [1792582245, 1792582200, 1792582300]
for ts in sorted(events):
print(datetime.fromtimestamp(ts, tz=timezone.utc).strftime('%H:%M:%S'))
11:30:00 11:30:45 11:31:40
Сортировка чисел даёт тот же порядок, что и сортировка моментов, — но по числу вы ещё и получаете разницу между соседними простым вычитанием, без timedelta. Мы так делали в счётчиках: прирост за час считали как ts_now - ts_prev, а из уже разобранных дат собирали только то, что показываем человеку.
| Что лежит в поле | Пример | Когда берут |
|---|---|---|
| BIGINT, секунды | 1792593045 | счётчики, ключи, сравнение в базе |
| BIGINT, миллисекунды | 1792593045000 | то, что отдаёт JavaScript |
| TIMESTAMP с поясом | 2026-10-21 14:30:45+00 | когда нужны даты и арифметика прямо в SQL |
| Строка ISO 8601 | 2026-10-21T14:30:45+00:00 | JSON и выгрузки, где формат важнее скорости |
Выбор между ними — не вопрос вкуса, а вопрос того, что вы будете делать с данными. Число сравнивается, сортируется и складывается без единого преобразования, но человек его прочитать не может. ISO-строка читается глазами и едет в JSON без потерь, но сортировать её придётся осторожно: одинаковый формат ещё не гарантия одинаковой длины, а часовой пояс в строке без разбора текста не виден. Мы в базе держим TIMESTAMP, наружу отдаём ISO — подробнее об этом в уроке про isoformat — а для счётчиков и ключей держим отдельное BIGINT-поле.
Unix-время живёт и без datetime
- заголовок Date в HTTP-ответе — тот же самый момент, но в формате RFC 1123;
- поля iat и exp в JWT — целые секунды с эпохи, их и проверяет сервер аутентификации;
- атрибут Max-Age у cookie — целое число секунд, а не дата;
- метрики и метки в системах мониторинга — миллисекунды с начала эпохи Unix.
Ни один из этих форматов не требует datetime вообще. Но тот, кто их читает, рано или поздно захочет посчитать «сколько секунд осталось» или «что было раньше» — и вот тут уже нужны либо арифметика на числах, либо один-единственный вызов timestamp() с явным поясом. Ровно эту границу и держит правило урока: число — сам момент, а пояс решает, какими цифрами момент покажется на часах.
from datetime import datetime, timezone
# Устаревший способ получить время UTC: datetime.utcnow()
# Актуальный способ: datetime.now(timezone.utc)
print(datetime.now(timezone.utc))
Что дальше
Всё, что разобрали выше, работает, пока вы сравниваете моменты, записанные в одном поясе. Как только в одной таблице встречаются записи из разных зон — а в логах они встречаются всегда — число перестаёт быть достаточным. Дальше — урок про часовые пояса: там naive-дата противётся aware, а astimezone переносит один и тот же момент в разные цифры на часах.
Сначала предскажи ответ в голове — это главный навык программиста.
from datetime import datetime, timezone
epoch = datetime.fromtimestamp(0, tz=timezone.utc)
print(epoch.year)
from datetime import datetime, timezone
d = datetime(2026, 10, 21, 14, 30, 45, tzinfo=timezone.utc)
print(round(d.timestamp()))
from datetime import datetime, timedelta, timezone
utc = datetime(2026, 10, 21, 11, 30, 45, tzinfo=timezone.utc)
msk = datetime(2026, 10, 21, 14, 30, 45, tzinfo=timezone(timedelta(hours=3)))
print(round(msk.timestamp()) - round(utc.timestamp()))
from datetime import datetime, timezone
d = datetime(2026, 10, 21, 14, 30, 45, tzinfo=timezone.utc)
print(int(d.timestamp() * 1000))
from datetime import datetime, timezone
d = datetime(2026, 10, 21, 14, 30, 45, 500000, tzinfo=timezone.utc)
print(int(d.timestamp()))
print(round(d.timestamp()))
1. Что такое unix-время?
2. Зачем в fromtimestamp аргумент tz=timezone.utc?
3. Какой тип возвращает метод d.timestamp()?
4. Один момент записан в UTC и в UTC+3. Что вернут их timestamp()?
5. Что вернёт datetime.fromtimestamp(0, tz=timezone.utc)?
6. Чем int(ts) отличается от round(ts) для значения 1792593045.5?
7. Что произойдёт с int(-0.5) и почему это важно для дат до 1970 года?
8. Почему unix-время в микросекундах опасно хранить во float?
9. Что такое проблема 2038 года?
Возьмите aware-дату datetime(2026, 10, 21, 6, 0, tzinfo=timezone.utc). Посчитайте её timestamp, округлите round() и выведите число. Затем верните дату через fromtimestamp с tz=timezone.utc и выведите её.
Как получить timestamp даты в Python?
Методом d.timestamp() у объекта datetime: он вернёт число секунд с эпохи 1970 года как float. Считайте его только у aware-дат — с tzinfo, иначе результат зависит от пояса машины: datetime(2026, 10, 21, 14, 30, 45, tzinfo=timezone.utc).timestamp() всегда даст 1792593045.0.
Почему timestamp без пояса — ошибка?
Потому что naive-объект не знает своего пояса, и метод молча предполагает местное время той машины, где запущен код: сервер в UTC и ноутбук в Москве получат разные числа из одного 14:30. Ошибки при этом не будет — числа просто разъедутся между машинами. Спасают явный tzinfo у даты и tz= в fromtimestamp.
Что такое эпоха Unix и почему 1970 год?
Так исторически выбрали ноль отсчёта unix-времени: полночь 1 января 1970 года по UTC. Все моменты времени измеряются секундами от неё, поэтому число 0 — это ровно 1970-01-01 00:00:00+00:00, что показывает datetime.fromtimestamp(0, tz=timezone.utc).
Как перевести timestamp обратно в дату?
Методом datetime.fromtimestamp(ts, tz=timezone.utc) — с обязательным аргументом tz. Тогда число однозначно превратится в aware-дату, одинаковую на любой машине, и круговая проверка datetime.fromtimestamp(d.timestamp(), tz=timezone.utc) == d вернёт True.
Урок 14: часовые поясаКак перевести дату в миллисекунды unix-времени?
Умножьте timestamp на 1000 и возьмите int: int(d.timestamp() * 1000) даст 1792593045000 для 21 октября 2026 года 14:30:45 UTC. Микросекунды — умножение на 1 000 000, но там уже стоит следить за пределом точности float в 2**53 целых.
Урок 13: timestampЧто такое проблема 2038 года в Python?
Ограничение не Python, а типа данных: знаковый 32-битный int вмещает максимум 2147483647, а это 19 января 2038 года 03:14:07 UTC. Если timestamp хранится в колонке INT, после этой даты значения переполняются и прыгают назад к 1970 году. В базе под такие значения нужен BIGINT, а в JSON — целое число вместо float.
Почему timestamp возвращает float, а не целое число?
Объект datetime хранит микросекунды, поэтому метод возвращает дробное число: у даты 14:30:45.5 получится 1792593045.5. Если нужны целые секунды, применяйте round(ts) — причём round, а не int: int срезает к нулю и на отрицательных значениях до эпохи теряет секунду.
Можно ли хранить timestamp в JSON?
Да, но только как число: json.dumps объекта datetime падает с TypeError: Object of type datetime is not JSON serializable. Число уезжает без потерь, а если нужен читаемый вид — сначала превратите дату в строку через isoformat(), а на приёме разберите обратно через datetime.fromisoformat().
Понравился урок? Сошлитесь на него
«Число — сам момент, а пояс лишь решает, какими цифрами момент покажется на часах.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Datetime · Урок 12
weekday и isoweekday: дни недели
weekday() считает с нуля: 0 — понедельник. isoweekday() — с единицы, по стандарту ISO-8601. Достаём день недели из даты, переводим в «среду» и находим ближайший понедельник.
Datetime · Урок 14
Часовые пояса: timezone и offset
Naive-дата не знает своего пояса, aware — знает: tzinfo хранит смещение от UTC. Один момент в двух поясах — разные цифры на часах и одно и то же время.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Pandas · Урок 9
Даты и временные ряды в Pandas: resample и rolling
Строки «01.07.2024» превращаются в настоящие даты, выручка агрегируется по неделям и месяцам через resample, а шум сглаживает rolling — строим тренд продаж магазина.
даты и время в pandas to_datetime примерыset_index datetime pandas
openpyxl · Урок 9
Даты в Excel через openpyxl: запись, формат и арифметика
Пишем в ячейку настоящий datetime, показываем его по-русски маской DD.MM.YYYY и считаем дни между датами — а заодно узнаём, почему дату из файла нельзя сравнивать со строкой.
openpyxl datetimedatetime в excel
Datetime · Урок 1
datetime в Python: первый момент времени
Дата как строка не умеет ничего: ни сравниться, ни посчитаться. Создаём первый объект datetime и разбираем его на поля — от года до секунды.
datetime pythondatetime в python