У темі про файли ми навчилися
читати журнал програми рядок за рядком. Майже кожен такий рядок починається з мітки часу —
і поки вона лишається текстом, з нею нічого не зробиш: не віднімеш, не порівняєш, не
відсортуєш надійно. Щоб мітка стала величиною, її треба перетворити на обʼєкт. Цим і
займається модуль datetime.
Наскрізний приклад цієї лекції — одне замовлення в невеликій крамниці. Замовлення №4417, оформлене 28 березня 2026 року о 14:05:09 за київським часом. Ми будемо його зберігати, показувати клієнту, додавати до нього строк доставки й порівнювати з іншими замовленнями. І на кожному з цих кроків знайдеться пастка.
01 / ТипиТри типи й що в них лежить
Модуль datetime дає три типи для позначення часу. Різниця між ними
проста: скільки саме з календаря вони знають.
| тип | що зберігає | приклад | для чого |
|---|---|---|---|
| date | рік, місяць, день | date(2026, 3, 28) | дата народження, дедлайн |
| time | година, хвилина, секунда, мікросекунда | time(14, 5, 9) | час відкриття, розклад |
| datetime | усе разом | datetime(2026, 3, 28, 14, 5, 9) | момент події |
Плюс четвертий тип — timedelta. Він стоїть окремо, бо позначає не момент,
а проміжок: «три доби», «сорок хвилин». До нього ми повернемось у розділі 06.
Розтин: що насправді всередині
Тут варто зупинитися, бо інтуїція новачка й реальність розходяться. Здається, що
datetime усередині — це велике число секунд, як показує годинник комп'ютера.
Насправді ні. CPython зберігає окремі календарні поля, спаковані
в десять байтів:
Плюс окреме посилання на tzinfo — обʼєкт поясу, який може бути й порожнім.
Разом sys.getsizeof(datetime(2026, 3, 28, 14, 5)) дає 48 байтів; у
date — 32, бо полів менше.
Із цього устрою випливають три речі, які інакше довелося б запамʼятовувати окремо.
Перша: діапазон типу — від 0001-01-01 до
9999-12-31, бо під рік відведено рівно два байти. Друга:
роздільність — рівно одна мікросекунда, і наносекунди в datetime записати
неможливо. Третя, найважливіша: поле tzinfo —
необовʼязкове. Обʼєкт цілком коректний і без нього. Саме ця «дірка» й породжує
головну тему лекції.
02 / СтворенняСтворення, поля, now()
Створюють обʼєкти напряму — переліком чисел у порядку від більшого до меншого:
замовлення = datetime(2026, 3, 28, 14, 5, 9)
Порядок аргументів той самий, що в ISO-запису: рік, місяць, день, година, хвилина,
секунда. Обовʼязкові лише перші три — решта за замовчуванням нулі. Читаються поля
через крапку: замовлення.year дає 2026,
замовлення.month — 3, замовлення.hour — 14.
Обʼєкти незмінні (immutable) — так само, як
кортежі. Присвоїти замовлення.hour = 15
не вийде, буде AttributeError. Щоб отримати змінений момент, роблять копію
методом replace: замовлення.replace(hour=15) повертає
новий обʼєкт, а старий лишається цілим. Це зручно: момент, який ти передав
у функцію, ніхто не зіпсує потай.
Поточний час беруть двома способами: date.today() дає сьогоднішню дату,
datetime.now() — момент прямо зараз. Обидва питають системний годинник
і локальні налаштування машини.
now()
всередині логіки. Функція, яка сама питає годинник, дає різний результат щохвилини —
таку не перевіриш тестом і не відтвориш чужу
помилку. Передавай момент аргументом:
def прострочено(замовлення, зараз):. Тоді в бою ти передаси
datetime.now(...), а в тесті — фіксовану дату, і поведінка стане
передбачуваною. Практика до цієї теми написана саме так: жодного
now(), усі дати зашиті, тому зошит дає однаковий вивід при кожному запуску.03 / НаївніНаївні проти обізнаних
Ось головна ідея всієї теми, і вона коштує того, щоб прочитати цей розділ двічі.
Обʼєкт datetime буває у двох станах:
- наївний (naive) — поле
tzinfoпорожнє. Обʼєкт знає, що написано на циферблаті, і не знає, чий це циферблат; - обізнаний (aware) —
tzinfoзаповнене. Обʼєкт знає і напис, і пояс, а отже однозначно вказує на мить у світовій історії.
Побутова аналогія: наївний момент — це фотографія настінного годинника без підпису, де зроблена. «14:05» — і все. Обізнаний — та сама фотографія з підписом «Київ». Тільки з підписом можна відповісти, що було в цю мить у Токіо.
Python не дозволяє змішувати ці два світи. Спроба порівняти наївний з обізнаним
дає TypeError — і це не примха, а захист. Порівняти їх
чесно неможливо: щоб сказати, який момент раніший, треба знати обидва пояси, а один
із них не названо. Мова радше зупиниться, ніж вгадає.
Інтерактив 1 · Наївний проти обізнаного
Два замовлення на 14:05:09. Дивись, чи вдається покласти їх на світову вісь часу.
TypeError замість того, щоб вигадати відповідь.< кидає TypeError, а от == — ні: він тихо
повертає False. Логіка тут своя: рівність зобовʼязана відповідати на
будь-яку пару обʼєктів (інакше зламався б пошук у
множинах і словниках),
і «різні за природою — отже не рівні» є законною відповіддю. Але для коду це найгірший
із можливих варіантів: перевірка if момент == дедлайн: мовчки не спрацює
жодного разу, і жодної помилки ти не побачиш.04 / utcnowЧому прибрали utcnow()
Десятиліттями стандартним способом узяти світовий час був
datetime.utcnow(). У Python 3.12 він оголошений застарілим
(deprecated) і чекає на видалення. Причина рівно та, про яку ми щойно говорили.
utcnow() повертає наївний обʼєкт, у полях якого лежить
час за UTC. Тобто обʼєкт, який містить світовий час, але не каже про це. Це
обман за замовчуванням, і він породжує дві помилки, що трапляються найчастіше.
Поруч у коді хтось написав datetime.now() — теж наївний обʼєкт,
але в його полях місцевий київський час. Обидва наївні, тому Python порівняє їх
без жодної помилки — просто зіставить цифри. А цифри розходяться
на дві-три години. Логіка «чи прострочено замовлення» тихо бреше півроку, потім
переходить на літній час і починає брехати на іншу величину.
Метод .timestamp() на наївному обʼєкті вважає, що в полях
місцевий час, і віднімає місцеве зміщення. Подай йому результат
utcnow() — і отримаєш число, зсунуте рівно на пояс. Мітка часу в базі
даних поїде на дві години, і ніхто не помітить, поки клієнт не поскаржиться.
зараз = datetime.now(timezone.utc)
Той самий момент, але обʼєкт обізнаний: у ньому написано
+00:00. Тепер помилка 1 неможлива — при спробі порівняти його з наївним
Python зупинить програму, а не мовчки видасть неправильну відповідь. І помилка 2
неможлива теж: .timestamp() бачить пояс і рахує точно.
utcnow() прибирають
не тому, що він рахував неправильно — рахував він бездоганно. Його прибирають тому,
що він викидав інформацію: знав пояс і не поклав його в результат.
Це загальне правило проєктування — якщо функція знає щось важливе про свій результат,
вона мусить це віддати, а не лишити користувачеві здогадуватись.05 / ПоясиПояси через zoneinfo
Звідки взяти пояс? З Python 3.9 у стандартній бібліотеці є модуль
zoneinfo, який читає системну базу часових поясів IANA — ту саму, якою
користуються Linux, macOS і мільйони серверів.
київ = ZoneInfo("Europe/Kyiv")
замовлення = datetime(2026, 3, 28, 14, 5, 9, tzinfo=київ)
Ім'я поясу — це завжди Регіон/Місто, і воно позначає не зміщення,
а набір правил: яке зміщення діяло в цій місцевості в кожен момент
історії. У «Europe/Kyiv» записано і +02:00 узимку, і +03:00 улітку, і дати всіх
переходів за багато десятиліть.
Тому ZoneInfo("Europe/Kyiv") і timezone(timedelta(hours=2))
— це різні речі, і плутати їх дорого. Друге — фіксовані «плюс дві години
назавжди»: у липні воно дасть неправильний київський час. Фіксоване зміщення годиться
рівно для двох випадків: UTC (він і є фіксований) і розбір мітки, у якій зміщення вже
написане числом.
Переведення між поясами робить метод astimezone. Він
не змінює момент — він переписує той самий момент іншим циферблатом:
→ 2026-03-28 21:05:09+09:00
Інтерактив 2 · Одна мить у чотирьох містах
Повзунки задають момент за UTC. Кожен рядок — місцевий циферблат того самого моменту.
06 / timedeltatimedelta — це різниця
Відніми один момент від іншого — отримаєш не число, а обʼєкт timedelta:
→ timedelta(days=2, seconds=35691)
Це та сама ідея, що з числами: різниця двох величин має інший тип, ніж самі величини. Момент часу й проміжок — різні сутності. Момент плюс момент безглуздий (де середина між двома датами? питання не про час), а момент плюс проміжок — цілком осмислений і дає новий момент.
Усередині timedelta зберігає лише три числа: дні,
секунди й мікросекунди. Усе інше, що ти передаси — години, хвилини, тижні — одразу
переводиться в них і зникає. Тому timedelta(hours=24) і
timedelta(days=1) — це буквально той самий обʼєкт, і перевірка
на рівність дасть True.
Чому немає місяців і років
Спроба написати timedelta(months=1) обривається помилкою:
TypeError: 'months' is an invalid keyword argument. Це найчастіше
запитання новачка — і відповідь тут не «забули додати».
Місяць не має довжини. Точніше, має аж чотири різні: 28, 29, 30 або 31 день. Тому «плюс один місяць» не є проміжком часу — це календарна операція над датою, і вона не має єдиної правильної відповіді. Скільки буде 31 січня плюс місяць? 28 лютого? 3 березня? 29 лютого у високосний рік? Кожна відповідь має прихильників, і жодна не є арифметичною. Рік не кращий: 365 або 366 днів.
Стандартна бібліотека відмовляється вгадувати. Якщо тобі справді потрібні календарні
місяці — або рахуй поля вручну через replace(month=…), або бери
зовнішню бібліотеку dateutil з її relativedelta, де ця
неоднозначність задокументована й керована.
Інтерактив 3 · Арифметика з timedelta
Скільки додати до замовлення 4417. Дивись, як Python нормалізує проміжок.
07 / Літній часДоба не завжди 24 години
Тепер найкоштовніша пастка теми — та, що двічі на рік породжує помилки «рівно на годину». Двічі на рік місцева доба перестає бути 24-годинною.
У 2026 році Київ переводить годинники 29 березня: о третій ночі стрілки стрибають одразу на четверту. Година «03:xx» того дня не існує — доба має 23 години. І 25 жовтня: о четвертій ранку стрілки повертаються на третю. Година «03:xx» того дня трапляється двічі — доба має 25 годин.
Тут і починається найцікавіше, бо Python поводиться не так, як очікує більшість.
Арифметика над обізнаним моментом — «настінна» (wall-clock): додаючи
timedelta(days=1), Python додає одиницю до поля «день» і лишає циферблат
на місці, а зміщення перераховує вже потім. Тому:
старт + timedelta(days=1) → 2026-03-29 14:05+03:00
На циферблаті знову 14:05 — «завтра о тій самій порі». Але зміщення змінилось з +02 на +03, тобто реально минуло лише 23 години.
→ 2026-03-29 15:05+03:00
Минуло рівно 24 години фізичного часу — але циферблат показує вже 15:05.
Обидві відповіді правильні. Просто це відповіді на різні запитання: «завтра о тій самій порі» й «через добу від цієї миті» — не одне й те саме, і різниця стає видимою двічі на рік. Вибирати треба свідомо: нагадування будильника хоче спосіб 1, вимірювання тривалості — спосіб 2.
Інтерактив 4 · Доба, у якій 23 або 25 годин
Кожна клітинка — година місцевого циферблата. Полічи їх у другому рядку.
datetime(2026,3,29,14,5,tzinfo=київ) − datetime(2026,3,28,14,5,tzinfo=київ)
дасть рівно 1 day, хоча фізично минуло 23 години. Щоб отримати справжню
тривалість, обидва моменти треба спершу перевести в UTC — тоді різниця буде
23:00:00. Правило просте: вимірюєш тривалість — рахуй у UTC.fold: fold=0 — перше проходження, fold=1 — друге.
Але == між ними всередині одного поясу дає True, хоча
.timestamp() у них відрізняється на 3600. А весняна «неіснуюча» 03:30
створюється без помилки й тихо перетворюється на 01:30 UTC. Це не баги, а свідомі
компроміси стандарту — і ще один доказ, що зберігати треба UTC.08 / КалендарВисокосний рік
Друга календарна нерівномірність — лютий. Земля обертається навколо Сонця приблизно за 365.2422 доби, тобто майже на чверть доби довше за календарний рік. Юлій Цезар компенсував це додатковим днем раз на чотири роки — але чверть не точна, і календар почав відставати приблизно на добу за 128 років. Григоріанська реформа додала два винятки: рік, кратний 100, високосним не є, а кратний 400 — таки є.
Тому 2000-й був високосним, а 1900-й і 2100-й — ні. Реальна ціна цієї дрібниці: у 2000 році чимало систем помилилися саме на правилі «÷400», бо програмісти памʼятали лише перші два поділи.
Інтерактив 5 · Три поділи, один зайвий день
Поводи роком через 1900 і 2000 — правила спрацьовують по-різному.
09 / Форматstrftime і strptime
Обʼєкт треба показати людині — і прочитати назад із тексту. За це відповідають дві дзеркальні операції з майже однаковими іменами:
момент.strftime(шаблон)— format: обʼєкт → рядок;datetime.strptime(рядок, шаблон)— parse: рядок → обʼєкт.
Літера f — від format, p — від parse. Обидві беруть той
самий шаблон: звичайний текст, у якому % із літерою позначає поле.
| код | що означає | на 2026-03-28 14:05:09 |
|---|---|---|
| %Y | рік, чотири цифри | 2026 |
| %y | рік, дві цифри | 26 |
| %m | місяць числом, з нулем | 03 |
| %d | день місяця, з нулем | 28 |
| %H | година, 00–23 | 14 |
| %M | хвилина | 05 |
| %S | секунда | 09 |
| %B | назва місяця повністю | March |
| %A | назва дня тижня | Saturday |
| %j | день від початку року | 087 |
| %% | сам знак відсотка | % |
Про %B і %A варто знати одразу: вони залежать від
локалі процесу. За замовчуванням буде англійська — March,
а не березня. Українські назви доводиться робити самому, звичайним
списком назв і звертанням за
момент.month - 1. Це нудно, зате передбачувано й не залежить від
налаштувань чужого сервера.
Інтерактив 6 · Конструктор шаблону
Склади шаблон із кодів — і подивись, що дасть форматування і що вціліє при розборі назад.
клацай коди — вони додаються в кінець
strptime підставить туди нулі.03/04/2026 — це 3 квітня
чи 4 березня? У Британії перше, у США друге. Тому %d/%m/%Y і
%m/%d/%Y обидва «спрацюють» на такому рядку й дадуть різні дати —
без жодної помилки. Це класична причина мовчазного псування даних при імпорті чужих
таблиць. Захист один: не приймай неоднозначні формати, а якщо мусиш — перевір
на даті з числом більшим за 12.10 / ISOISO 8601
Із нескінченної кількості способів записати дату міжнародний стандарт ISO 8601 виділяє один. У Python він доступний без жодних шаблонів:
datetime.fromisoformat('2026-03-28T14:05:09+02:00') → назад той самий обʼєкт
Формат виглядає незвично («що це за T посередині?»), але кожна його риса
розвʼязує конкретну проблему:
- від більшого до меншого: рік, місяць, день. Тому лексикографічне порівняння рядків збігається з хронологічним — про це наступний розділ;
- фіксована ширина з нулями: завжди чотири цифри року й дві місяця,
тому
03ніколи не опиниться після11; Tяк розділювач: однозначно ділить дату й час без пробілу, який ламає розбір командного рядка й CSV;- зміщення в кінці:
+02:00абоZдля UTC. Мітка сама себе описує й не залежить від того, хто її читає.
Практичний висновок простий: для зберігання й обміну — тільки ISO.
У базі даних, у JSON, у файлі журналу, у назві файлу з резервною копією. Формат
%d.%m.%Y — це формат показу людині, і він має жити лише в тому
рядку коду, де ти малюєш екран.
fromisoformat умів читати рівно те, що видавав isoformat, і
спотикався на '2026-03-28T12:05:09Z' — літера Z його ламала,
хоча це найпоширеніший запис UTC у вебі. З 3.11 підтримка стала повною: і
Z, і скорочені форми читаються. Якщо код має працювати на старіших
версіях — Z заздалегідь замінюють на +00:00.11 / СортуванняЧому дати не сортують як рядки
Тепер закриємо старий борг. Ти вже вмієш сортувати списки, і спокуса очевидна: дати в журналі й так лежать текстом, навіщо перетворювати?
Бо сортування рядків посимвольне. Python порівнює перший символ,
при рівності — другий, і так далі; про календар він при цьому не знає нічого. Для
запису 28.03.2026 першим символом виявляється день, тому весь список
шикується за днем місяця: 02 листопада стане раніше за 31 грудня, бо «0» менше за «3».
З ISO-записом виходить навпаки — і це не щаслива випадковість, а те, заради чого формат так і збудували. Старший розряд стоїть першим, ширина полів стала, отже посимвольне порівняння збігається з хронологічним. Це єдиний формат дати, рядки якого можна сортувати безкарно.
Інтерактив 7 · Сортування рядків проти сортування дат
Шість дат із журналу. Перемикай формат запису й дивись на результат sorted().
sorted(). Цифра в кружечку це справжній хронологічний
номер запису. Якщо праворуч кружечки читаються 1, 2, 3, 4, 5, 6 згори вниз — сортування
правильне. Варіант «ISO без нулів» показує, що самого лише порядку «рік-місяць-день»
замало: без ведучих нулів 2026-11-2 стає меншим за 2026-3-5.12 / Unix-часUnix-час і як зберігати
Є ще один спосіб позначити мить — одним числом. Unix-час (POSIX time)
— це кількість секунд, що минули від 1970-01-01 00:00:00 UTC. Момент
нашого замовлення в цьому вигляді — 1774699509.
datetime.fromtimestamp(1774699509, timezone.utc) → 2026-03-28 12:05:09+00:00
Головна перевага числа — воно не може бути наївним. Точка відліку зафіксована в самому визначенні, тому двозначності не існує в принципі. Плюс числа легко порівнювати, віднімати й зберігати в будь-якій базі.
Недоліки теж є, і серйозні. Число нечитабельне для людини. Воно не знає ані поясу, ані календаря, тому «додати місяць» до нього неможливо. І в ньому немає високосних секунд: за визначенням у добі рівно 86400 секунд, тож у рідкісні дні з високосною секундою unix-час просто повторює одне значення двічі.
Правило, на якому тримається все
Із усього, що ми розібрали, випливає одна архітектурна порада, і вона варта окремого рядка:
Розгорнуто це означає ось що. У базу, у журнал, у файл іде обізнаний момент за UTC
(або unix-час — це те саме іншими символами). Уся арифметика — тривалості, порівняння,
сортування — теж рахується в UTC, де доба завжди 24 години й переходів не буває.
І лише в останню мить, коли треба намалювати екран чи надіслати лист, момент
переводиться в пояс конкретного користувача через astimezone і
форматується його звичним записом.
Порушення цього правила й дає ті самі помилки «на годину», що зʼявляються двічі на рік. Механіка їх завжди однакова: десь час зберегли в місцевому поясі, а перерахували вже після переходу — і зміщення виявилось іншим, ніж було в момент запису. Якщо в базі лежить UTC, такої розбіжності не буває фізично: там немає що зсувати.
13 / ПідсумокЩо з цього забрати
П'ять пунктів, які варто памʼятати навіть через рік:
- Наївний час — це напис без адреси. Він не позначає мить, і
порівняти його з обізнаним не можна:
TypeErrorтут рятує, а не заважає; datetime.now(timezone.utc)замістьutcnow(). Старий метод повертав UTC і мовчав про це — саме тому його прибирають;timedelta— це різниця, а не момент. Місяців і років у ньому немає, бо в них немає сталої довжини;- Доба буває 23 і 25 годин. Арифметика над обізнаним моментом настінна; вимірюєш тривалість — переводь у UTC;
- ISO 8601 для зберігання, звичний формат для показу. Тільки ISO-рядки можна сортувати як текст, і то лише завдяки ведучим нулям.
І загальна думка в дусі цього курсу. Час здається найпростішою величиною у світі —
і виявляється найзаплутанішою, бо він не фізичний, а домовлений. Кордони
поясів, переходи на літній час, високосні роки й секунди — це рішення людей, які
регулярно змінюються урядами. Тому zoneinfo й читає базу з диска, а не
рахує за формулою: формули для цього не існує. Уміння бачити за міткою часу дві різні
речі — фізичну мить і людський напис про неї — і є та відмінність, з якої починається
надійна робота з датами.
strptime вимагає точного
шаблону. А в practice.ipynb ти порахуєш вік у днях,
розберешся з поясами вручну, зламаєш порівняння наївного з обізнаним і полагодиш його.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.