Модуль datetime работает с моментами на календаре: датой, временем и промежутком между ними. Если задача звучит как «перевести 609 секунд в 10:09», datetime не нужен вовсе, это форматирование длительности обычной арифметикой. Здесь про календарь. Весь код выполнен на CPython 3.14.5.
Тип выбирается по смыслу значения
Четыре типа, которые надо различать
Модуль даёт четыре класса, и путаница между ними стоит дороже всего остального:
from datetime import date, time, datetime, timedelta
print(date(2026, 6, 3))
print(time(14, 30, 5))
print(datetime(2026, 6, 3, 14, 30, 5))
print(timedelta(days=2, hours=3))
2026-06-03
14:30:05
2026-06-03 14:30:05
2 days, 3:00:00
date хранит только календарную дату, time только время суток без привязки к дню, datetime то и другое сразу. timedelta стоит особняком: это не момент, а промежуток, и складывать его с датами можно, а сравнивать с ними нельзя.
Из готового datetime части достаются атрибутами и методами:
from datetime import datetime
d = datetime(2026, 6, 3, 14, 30, 5)
print(d.year, d.month, d.day, d.hour, d.minute, d.second)
print(d.date(), d.time())
print(d.weekday(), d.isoweekday())
2026 6 3 14 30 5
2026-06-03 14:30:05
2 3
weekday() считает дни с нуля и в среду даёт 2, isoweekday() считает с единицы и даёт 3. Перепутать их легко, и ошибка вылезет только на конкретном дне недели.
Как получить текущую дату и время
Три вызова на все случаи: datetime.now() для локального времени, date.today() для сегодняшней даты, datetime.now(timezone.utc) для UTC.
from datetime import datetime, date, timezone
print(datetime.now())
print(date.today())
print(datetime.now(timezone.utc))
Вывод зависит от момента запуска: datetime.now() даёт что-то вроде 2026-06-03 14:30:05.123456 вместе с микросекундами, а последняя строка добавляет к времени смещение +00:00.
Метод datetime.utcnow(), который до сих пор встречается в старых руководствах, использовать нельзя:
import warnings
from datetime import datetime
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
datetime.utcnow()
print(caught[0].category.__name__)
print(caught[0].message)
DeprecationWarning
datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
Обёртка вокруг вызова нужна только затем, чтобы предупреждение точно напечаталось: по умолчанию Python показывает DeprecationWarning не всегда, и в чужой библиотеке ты его не увидишь вовсе.
Проблема не в самом устаревании, а в том, что utcnow() возвращал время UTC, но помечал его как «без пояса». Такой объект выглядел локальным временем, и при следующем преобразовании смещение применялось второй раз. Правильная замена — datetime.now(timezone.utc), она сразу отдаёт объект с поясом.
Как превратить дату в строку
Метод strftime собирает строку по шаблону из кодов:
from datetime import datetime
d = datetime(2026, 6, 3, 14, 30, 5)
print(d.strftime("%d.%m.%Y"))
print(d.strftime("%Y-%m-%d %H:%M:%S"))
print(d.strftime("%d.%m.%Y %H:%M"))
print(f"{d:%d.%m.%Y}")
03.06.2026
2026-06-03 14:30:05
03.06.2026 14:30
03.06.2026
Коды запоминаются по смыслу: %Y это год четырьмя цифрами, %m месяц, %d день, %H часы в 24-часовом формате, %M минуты, %S секунды. Заглавная и строчная буквы означают разное: %M это минуты, а %m месяц, и перепутать их можно ровно один раз.
Последняя строка показывает короткую форму: тот же шаблон работает прямо внутри f-строки после двоеточия, без вызова метода.
Для машинного обмена данными формат не выдумывают, а берут ISO 8601:
from datetime import datetime, date
d = datetime(2026, 6, 3, 14, 30, 5)
print(d.isoformat())
print(date(2026, 6, 3).isoformat())
print(str(d))
2026-06-03T14:30:05
2026-06-03
2026-06-03 14:30:05
Разница между isoformat() и str() в разделителе: буква T против пробела. В API и в JSON ждут первый вариант.
Как разобрать строку в дату
strptime делает обратное: принимает строку и тот же шаблон, возвращает datetime.
from datetime import datetime
print(datetime.strptime("03.06.2026", "%d.%m.%Y"))
print(datetime.strptime("2026-06-03 14:30", "%Y-%m-%d %H:%M"))
2026-06-03 00:00:00
2026-06-03 14:30:00
Первая строка получила нули вместо времени: в исходной строке его не было, а тип всё равно datetime. Когда нужна именно дата, добавляют .date().
Шаблон обязан совпасть со строкой посимвольно, включая разделители, иначе разбор падает и его приходится перехватывать:
from datetime import datetime
try:
datetime.strptime("03/06/2026", "%d.%m.%Y")
except ValueError as exc:
print(f"ValueError: {exc}")
ValueError: time data '03/06/2026' does not match format '%d.%m.%Y'
Сообщение показывает и строку, и шаблон, поэтому расхождение видно сразу. Здесь это косые черты вместо точек.
Для ISO-строк шаблон не нужен, есть отдельный конструктор:
from datetime import datetime, date
print(datetime.fromisoformat("2026-06-03T14:30:05"))
print(date.fromisoformat("2026-06-03"))
print(datetime.fromisoformat("2026-06-03 14:30:05+03:00"))
2026-06-03 14:30:05
2026-06-03
2026-06-03 14:30:05+03:00
Третья строка разобрала смещение пояса и сохранила его. Разбирать ответы API стоит именно так: короче, быстрее и без шанса ошибиться в шаблоне.
Как прибавить дни и посчитать разницу
Всё, что связано со сдвигом во времени, делает timedelta:
from datetime import datetime, date, timedelta
d = datetime(2026, 6, 3, 14, 30)
print(d + timedelta(days=7))
print(d - timedelta(hours=3))
print(date(2026, 6, 3) + timedelta(days=30))
2026-06-10 14:30:00
2026-06-03 11:30:00
2026-07-03
Переход через границу месяца считается сам: 3 июня плюс 30 дней даёт 3 июля, и думать о числе дней в июне не надо.
Вычитание двух моментов даёт timedelta:
from datetime import datetime
a = datetime(2026, 6, 3, 14, 30)
b = datetime(2026, 5, 28, 9, 0)
diff = a - b
print(diff)
print(diff.days, diff.seconds, diff.total_seconds())
6 days, 5:30:00
6 19800 538200.0
Здесь прячется ловушка, на которой ошибаются постоянно. Атрибут .seconds содержит не всю разницу, а только остаток сверх целых суток: 19800 секунд это 5 часов 30 минут. Полную длину промежутка даёт .total_seconds(), и она равна 538200. Считаешь «сколько всего секунд прошло» через .seconds — теряешь по 86400 за каждые сутки.
Разницу в днях удобнее считать по датам, без времени:
from datetime import date
print((date(2026, 6, 3) - date(2026, 5, 28)).days)
6
Складывать две даты между собой нельзя, и это логично: сумма двух моментов календаря не значит ничего.
from datetime import date
try:
date(2026, 6, 3) + date(2026, 5, 28)
except TypeError as exc:
print(f"TypeError: {exc}")
TypeError: unsupported operand type(s) for +: 'datetime.date' and 'datetime.date'
Часовые пояса: naive и aware
Объект datetime бывает двух сортов. Naive не знает своего пояса, aware знает. Смешивать их запрещено:
from datetime import datetime, timezone
naive = datetime(2026, 6, 3, 14, 30)
aware = datetime(2026, 6, 3, 14, 30, tzinfo=timezone.utc)
try:
print(naive < aware)
except TypeError as exc:
print(f"TypeError: {exc}")
TypeError: can't compare offset-naive and offset-aware datetimes
Запрет спасает от молчаливой ошибки: два одинаковых на вид времени могут отличаться на несколько часов, и сравнивать их без пояса значит гадать.
Пояса берут из стандартного модуля zoneinfo по названию из базы IANA:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
msk = datetime(2026, 6, 3, 14, 30, tzinfo=ZoneInfo("Europe/Moscow"))
print(msk)
print(msk.astimezone(timezone.utc))
print(msk.utcoffset())
2026-06-03 14:30:00+03:00
2026-06-03 11:30:00+00:00
3:00:00
Метод astimezone переводит момент в другой пояс, сохраняя саму точку во времени: 14:30 в Москве и 11:30 в UTC — это одна и та же секунда. Названия вида "Europe/Moscow" предпочтительнее фиксированного смещения, потому что база IANA знает историю переводов часов, а timezone(timedelta(hours=3)) не знает.
Практическое правило для бэкенда: хранить и считать в UTC, переводить в местный пояс только на выводе пользователю.
Ошибки, на которых спотыкаются
Месяцев и лет в timedelta нет.
from datetime import timedelta
try:
timedelta(months=1)
except TypeError as exc:
print(f"TypeError: {exc}")
TypeError: __new__() got an unexpected keyword argument 'months'
Так и задумано. Длина месяца непостоянна, поэтому «плюс месяц» не имеет единственного смысла, и решать за тебя, во что превращается 31 января, язык отказывается.
Подмена дня напрямую. Попытка получить конец февраля заменой номера месяца падает:
from datetime import date
try:
date(2026, 1, 31).replace(month=2)
except ValueError as exc:
print(f"ValueError: {exc}")
ValueError: day 31 must be in range 1..28 for month 2 in year 2026
Последний день месяца берут из модуля calendar, который сам знает про високосные годы:
import calendar
from datetime import date
y, m = 2026, 2
print(calendar.monthrange(y, m)[1])
print(date(y, m, calendar.monthrange(y, m)[1]))
28
2026-02-28
Метка времени в секундах. Обмен с внешними системами часто идёт числом, и переводы туда и обратно делаются двумя методами:
from datetime import datetime, timezone
d = datetime(2026, 6, 3, 14, 30, tzinfo=timezone.utc)
print(d.timestamp())
print(datetime.fromtimestamp(1780583400, tz=timezone.utc))
1780497000.0
2026-06-04 14:30:00+00:00
Аргумент tz в fromtimestamp обязателен по смыслу: без него результат получится в поясе машины, и та же метка на сервере и на ноутбуке даст разное время.
Сортировка дат. Объекты сравнимы между собой напрямую, поэтому отдельная функция-ключ не нужна:
from datetime import date
dates = [date(2026, 6, 3), date(2026, 5, 28), date(2026, 6, 20)]
print(sorted(dates))
print(max(dates), min(dates))
[datetime.date(2026, 5, 28), datetime.date(2026, 6, 3), datetime.date(2026, 6, 20)]
2026-06-20 2026-05-28
Обрати внимание на разницу вывода: print списка показывает представление объектов, а print одиночной даты показывает её строковую форму.
Частые вопросы
Как получить текущую дату в формате ДД.ММ.ГГГГ
date.today().strftime("%d.%m.%Y") либо короче через f-строку: f"{date.today():%d.%m.%Y}".
Как перевести строку в дату
datetime.strptime(строка, шаблон) для произвольного формата и datetime.fromisoformat(строка) для ISO 8601. Второй способ не требует шаблона и работает быстрее.
Как посчитать возраст или число полных лет
Вычитанием дат счёт не выйдет: в timedelta нет лет. Считают по календарю: today.year - born.year - ((today.month, today.day) < (born.month, born.day)). Вычитание булева значения убирает год, если день рождения в этом году ещё не наступил.
Чем date отличается от datetime
date хранит только календарную дату, datetime дату вместе со временем. Из второго первое достаётся методом .date(), обратно поднимается через datetime.combine(d, time_obj).
Почему разница дат показывает мало секунд
Скорее всего читается .seconds вместо .total_seconds(). Первый атрибут даёт остаток сверх целых суток и никогда не превышает 86399.
Что учить дальше
Даты почти всегда попадают в код вместе со словарями и циклами: разобрать ответ API, отсортировать записи по времени, сгруппировать по дню. В пути «Python с нуля» на Koddo под даты отведён отдельный пак задач с автопроверкой, а начать проще всего с форматирования длительности: там та же тема без часовых поясов, на чистой арифметике.