Модуль datetime в Python: даты, время, интервалы и часовые пояса

Модуль datetime в Python: даты, время, интервалы и часовые пояса Полезное

Модуль datetime — это модуль стандартной библиотеки Python для работы с датами, временем и промежутками между ними. Ставить ничего не нужно: достаточно import datetime или from datetime import .... Примеры ниже проверены на Python 3.14 (сентябрь 2026).

В модуле несколько классов, и их легко спутать. Короткий словарь:

  • date — календарная дата без времени: 24 сентября 2026;
  • time — время суток без даты: 14:30;
  • datetime — дата и время вместе, то есть конкретный момент: 24.09.2026 14:30;
  • timedelta — длительность, «сколько прошло»: 7 дней и 3 часа. Это не момент, а разница между моментами;
  • tzinfo / timezone — сведения о часовом поясе, которые можно прикрепить к time или datetime.

Разберем каждый класс, форматирование строк, арифметику дат и часовые пояса, а в конце — типичные ошибки.

Минимальный пример: все классы сразу

from datetime import date, time, datetime, timedelta

d = date(2026, 9, 24)                 # календарная дата
t = time(14, 30)                      # время суток без даты
moment = datetime(2026, 9, 24, 14, 30)  # дата + время
week = timedelta(days=7)              # длительность

print(d, t, moment, sep=" | ")
print("через неделю:", moment + week)
print("день недели:", d.isoweekday())  # 1 = понедельник ... 7 = воскресенье
print("как строка:", moment.strftime("%d.%m.%Y %H:%M"))

Результат:

2026-09-24 | 14:30:00 | 2026-09-24 14:30:00
через неделю: 2026-10-01 14:30:00
день недели: 4
как строка: 24.09.2026 14:30

По умолчанию print выводит даты в формате ISO 8601: год-месяц-день. Сложение datetime + timedelta дает новый datetime, а strftime превращает объект в строку нужного вида.

Класс Что хранит Пример создания Типичная задача
date год, месяц, день date(2026, 9, 24) дни рождения, дедлайны без времени
time часы, минуты, секунды, микросекунды time(14, 30) время начала занятия
datetime дата + время (+ пояс) datetime(2026, 9, 24, 14, 30) момент события, логи
timedelta дни, секунды, микросекунды timedelta(hours=36) «через 3 дня», «сколько осталось»
timezone фиксированное смещение от UTC timezone.utc хранение времени в UTC

Класс date: календарная дата

Конструктор принимает три обязательных аргумента: год, месяц, день.

from datetime import date

d = date(2026, 9, 24)
print(d.year, d.month, d.day)        # 2026 9 24
print(d.weekday(), d.isoweekday())   # 3 4 -> четверг
print(d.replace(day=1))              # первое число того же месяца
print(date.fromisoformat("2026-12-31"))
print(d.isoformat())
2026 9 24
3 4
2026-09-01
2026-12-31
2026-09-24

Обратите внимание на два способа узнать день недели: weekday() считает понедельник нулем, а isoweekday() — единицей. Метод replace не меняет исходный объект: объекты datetime неизменяемы, он возвращает новую дату.

Несуществующую дату создать нельзя — модуль проверяет календарь, включая високосные годы:

from datetime import date

d = date(2026, 2, 30)
ValueError: day 30 must be in range 1..28 for month 2 in year 2026

Если дата приходит от пользователя, оборачивайте создание в try/except ValueError и показывайте понятное сообщение. Текущую дату дает date.today() — подробно про получение «сейчас» разобрано в статье о текущей дате и времени в Python.

Класс time: время суток

time хранит часы, минуты, секунды и микросекунды. Все аргументы необязательны, пропущенные равны нулю.

from datetime import time

t = time(9, 5, 30, 250000)
print(t)                     # 09:05:30.250000
print(t.hour, t.minute, t.second, t.microsecond)
print(t.isoformat(timespec="minutes"))
print(time())                # все поля по умолчанию равны нулю
09:05:30.250000
9 5 30 250000
09:05
00:00:00

Важная граница: к time нельзя прибавить timedelta, потому что без даты непонятно, что делать при переходе через полночь. Для арифметики используйте datetime.

Класс datetime: конкретный момент

datetime объединяет дату и время. Обязательны год, месяц и день, остальные поля по умолчанию нулевые.

from datetime import date, time, datetime

start = datetime(2026, 9, 24, 19, 0)
print(start.date(), "|", start.time())       # разложить на части
print(datetime.combine(date(2026, 10, 1), time(10, 0)))  # собрать из частей
print(datetime.fromisoformat("2026-09-24T19:00:00+03:00"))
print(datetime(2026, 9, 24))                 # время по умолчанию 00:00
2026-09-24 | 19:00:00
2026-10-01 10:00:00
2026-09-24 19:00:00+03:00
2026-09-24 00:00:00

Текущий момент дают datetime.now() (локальное время компьютера) и datetime.now(timezone.utc) (время в UTC с поясом). С Unix-временем, то есть числом секунд от 1 января 1970 года UTC, работают timestamp() и fromtimestamp():

from datetime import datetime, timezone

ts = 1790000000
print(datetime.fromtimestamp(ts, tz=timezone.utc))
moment = datetime(2026, 9, 24, 14, 30, tzinfo=timezone.utc)
print(moment.timestamp())
2026-09-21 14:13:20+00:00
1790260200.0

Без аргумента tz метод fromtimestamp вернет локальное время машины, поэтому результат будет зависеть от настроек компьютера.

Класс timedelta: арифметика дат

Разность двух datetime — это timedelta. К моменту можно прибавить или вычесть длительность.

from datetime import datetime, timedelta

deadline = datetime(2026, 10, 1, 18, 0)
now = datetime(2026, 9, 24, 14, 30)

left = deadline - now
print(left)                          # timedelta
print(left.days, left.seconds)       # 7 дней и остаток в секундах
print(left.total_seconds() / 3600)   # всего часов

print(now + timedelta(hours=36))
print(now - timedelta(weeks=2))
print(timedelta(hours=-1))           # отрицательная длительность
7 days, 3:30:00
7 12600
171.5
2026-09-26 02:30:00
2026-09-10 14:30:00
-1 day, 23:00:00

Здесь две ловушки. Первая: left.seconds — это не все секунды, а только остаток после целых дней (12600 = 3,5 часа). Полную длительность дает total_seconds(). Вторая: отрицательная длительность хранится как «минус один день плюс 23 часа» — это не ошибка, а внутреннее представление.

Почему нельзя прибавить месяц

timedelta умеет дни, секунды, микросекунды, а также миллисекунды, минуты, часы и недели, которые пересчитываются в них. Месяцев и лет нет, потому что их длина плавает.

from datetime import date, timedelta

print(date(2026, 1, 31) + timedelta(months=1))
TypeError: __new__() got an unexpected keyword argument 'months'

Исправление — решить, что значит «плюс месяц» для 31 января, и записать это правило явно. Частый вариант — прижимать день к концу месяца:

import calendar
from datetime import date

def add_months(d: date, months: int) -> date:
    total = d.month - 1 + months
    year, month = d.year + total // 12, total % 12 + 1
    last_day = calendar.monthrange(year, month)[1]
    return d.replace(year=year, month=month, day=min(d.day, last_day))

print(add_months(date(2026, 1, 31), 1))   # 2026-02-28
print(add_months(date(2028, 1, 31), 1))   # 2028-02-29, високосный год
print(add_months(date(2026, 11, 15), 3))  # 2027-02-15
print(add_months(date(2026, 3, 31), -1))  # 2026-02-28

Это одно из возможных правил, а не стандарт: биллинг или банковский календарь могут считать иначе. В проектах для такой арифметики часто берут сторонний пакет python-dateutil с relativedelta.

strftime и strptime: дата в строку и обратно

Два метода легко перепутать по названию: strftime (f — format) превращает объект в строку, strptime (p — parse) разбирает строку в datetime.

from datetime import datetime

moment = datetime(2026, 9, 24, 14, 5, 9)
print(moment.strftime("%d.%m.%Y"))           # 24.09.2026
print(moment.strftime("%Y-%m-%d %H:%M:%S"))  # 2026-09-24 14:05:09
print(moment.strftime("%j-й день года, неделя ISO %V"))

parsed = datetime.strptime("24.09.2026 14:05", "%d.%m.%Y %H:%M")
print(parsed, type(parsed).__name__)
24.09.2026
2026-09-24 14:05:09
267-й день года, неделя ISO 39
2026-09-24 14:05:00 datetime
Код Значение Пример
%Y год, 4 цифры 2026
%m месяц, 01-12 09
%d день месяца, 01-31 24
%H час, 00-23 14
%M минуты, 00-59 05
%S секунды, 00-59 09
%j день года, 001-366 267
%A, %B название дня недели и месяца зависит от локали
%z смещение от UTC +0300

Названия дней и месяцев (%A, %B) берутся из текущей локали системы, поэтому на разных машинах могут выйти на английском или на русском. Для стабильного результата в логах и API используйте числовые коды или ISO-формат.

Если строка не совпадает с шаблоном, strptime падает:

from datetime import datetime

datetime.strptime("2026-09-24", "%d.%m.%Y")
ValueError: time data '2026-09-24' does not match format '%d.%m.%Y'

Исправление — шаблон под фактический вид строки: "%Y-%m-%d", а для ISO-строк проще datetime.fromisoformat. Опаснее тихая ошибка, когда исключения нет:

from datetime import datetime

moment = datetime(2026, 9, 24, 14, 5)
print(moment.strftime("%d.%M.%Y"))   # ошибка: %M - минуты, а не месяц
print(moment.strftime("%d.%m.%Y"))   # исправлено: %m - месяц
24.05.2026
24.09.2026

Код не упал, но вместо сентября в строке стоят минуты. Строчная m — месяц, заглавная M — минуты.

Часовые пояса: naive и aware, модуль zoneinfo

У datetime есть два режима. Naive (наивный) объект не знает пояса: datetime(2026, 9, 24, 19, 0) — это просто «19:00», неясно где. Aware (осведомленный) объект хранит tzinfo и однозначно указывает момент времени.

С Python 3.9 в стандартной библиотеке есть модуль zoneinfo, который берет правила поясов из базы IANA (Europe/Moscow, America/New_York и т. д.):

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

meeting = datetime(2026, 9, 24, 19, 0, tzinfo=ZoneInfo("Europe/Moscow"))
print(meeting)                                         # 2026-09-24 19:00:00+03:00
print(meeting.astimezone(ZoneInfo("Asia/Novosibirsk")))
print(meeting.astimezone(ZoneInfo("America/New_York")))
print(meeting.astimezone(timezone.utc))

now_utc = datetime.now(timezone.utc)   # aware-время "сейчас" в UTC
print(now_utc.tzinfo)
2026-09-24 19:00:00+03:00
2026-09-24 23:00:00+07:00
2026-09-24 12:00:00-04:00
2026-09-24 16:00:00+00:00
UTC

Метод astimezone не меняет момент, а показывает его на часах другого пояса. На Linux и macOS база поясов обычно есть в системе; на Windows ее ставят пакетом pip install tzdata, иначе ZoneInfo выдаст ZoneInfoNotFoundError. Сторонний pytz, который часто встречается в старых статьях, для нового кода не нужен.

Сравнивать naive и aware на больше/меньше и вычитать их друг из друга нельзя:

from datetime import datetime, timezone

naive = datetime(2026, 9, 24, 19, 0)
aware = datetime(2026, 9, 24, 16, 0, tzinfo=timezone.utc)
print(naive.tzinfo, aware.tzinfo)
print(naive < aware)
None UTC
TypeError: can't compare offset-naive and offset-aware datetimes

Коварнее проверка на равенство: naive == aware не падает, а молча возвращает False (так с Python 3.3), даже если часы совпадают, — условие просто никогда не сработает. Разность aware - naive тоже дает TypeError: can't subtract offset-naive and offset-aware datetimes.

Исправление — привести оба значения к одному виду: прикрепить пояс к наивному (naive.replace(tzinfo=ZoneInfo("Europe/Moscow")), если точно известно, что это московское время) или везде работать с aware-объектами.

Переход на летнее время

Арифметика внутри одного пояса идет «по настенным часам». В поясах с переходом на летнее время это расходится с реально прошедшим временем:

from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo

ny = ZoneInfo("America/New_York")
before = datetime(2026, 11, 1, 0, 30, tzinfo=ny)   # за 1,5 часа до перехода на зимнее время (в 2:00)
after = before + timedelta(days=1)

print(before, "->", after)
print("по часам на стене:", after - before)
real = after.astimezone(timezone.utc) - before.astimezone(timezone.utc)
print("реально прошло:", real)
2026-11-01 00:30:00-04:00 -> 2026-11-02 00:30:00-05:00
по часам на стене: 1 day, 0:00:00
реально прошло: 1 day, 1:00:00

1 ноября 2026 года в Нью-Йорке часы переводят назад, поэтому «сутки по календарю» длились 25 часов. Практическое правило: хранить и сравнивать время в UTC, а в местный пояс переводить только для показа пользователю.

Если не получилось: частые симптомы

Симптом Причина Что сделать
ValueError: day ... must be in range несуществующая дата проверить ввод, ловить ValueError
ValueError: time data ... does not match format шаблон strptime не совпадает со строкой сверить разделители и порядок полей
в дате стоят минуты вместо месяца %M вместо %m месяц — %m, минуты — %M
TypeError: can't compare offset-naive and offset-aware или == всегда False смешаны объекты с поясом и без привести все к aware
TypeError: ... unexpected keyword argument 'months' в timedelta нет месяцев своя функция или relativedelta
ZoneInfoNotFoundError на Windows нет базы поясов pip install tzdata
DeprecationWarning у datetime.utcnow() метод устарел с Python 3.12 datetime.now(timezone.utc)

Выводы

  • Модуль datetime встроен в Python: date — дата, time — время суток, datetime — момент, timedelta — длительность, timezone/ZoneInfo — пояс.
  • Арифметика идет через timedelta; месяцев и лет в нем нет, правило «плюс месяц» нужно задавать явно.
  • strftime форматирует объект в строку, strptime разбирает строку; %m — месяц, %M — минуты.
  • Для часовых поясов в новом коде — zoneinfo (Python 3.9+); время лучше хранить в UTC и переводить в местное только для показа.
  • Naive и aware объекты нельзя сравнивать между собой, а арифметика в поясе с переходом на летнее время считает по настенным часам.

Где применяется / связь с практикой

Освойте тему на практике

Даты встречаются почти в любом прикладном коде: сроки задач и подписок, расписания, отчеты за период, разбор логов, хранение событий в базе и API с ISO-строками. Ошибки с поясами и форматами — частая причина багов, которые проявляются только у части пользователей или раз в год. Работу со стандартной библиотекой, типами данных и исключениями на практических задачах разбирают на курсе «Python-разработчик. Базовый уровень». Посмотреть формат обучения можно на открытых уроках Otus.

FAQ

Чем import datetime отличается от from datetime import datetime?
В первом случае вы импортируете модуль и пишете datetime.datetime.now(), во втором — сразу класс и пишете datetime.now(). Путаница возникает, когда в одном файле смешаны оба варианта: имя datetime перезаписывается.

Как посчитать возраст в полных годах?
Вычтите годы и отнимите единицу, если день рождения в этом году еще не наступил: today.year - born.year - ((today.month, today.day) < (born.month, born.day)). Деление timedelta.days на 365 дает погрешность из-за високосных лет.

Можно ли хранить дату в базе строкой?
Можно, если это ISO 8601 в одном смещении, лучше в UTC (2026-09-24T14:30:00+00:00): такие строки сортируются правильно и читаются через fromisoformat. Строки с разными смещениями сортируются как текст, а не по моменту: 2026-09-24T02:00:00+03:00 (23:00 UTC накануне) окажется после 2026-09-24T00:30:00+00:00. Но если СУБД поддерживает собственный тип даты и времени, лучше использовать его.

OTUS Журнал