Проект: трекер дедлайнов
Финальный проект раздела: список задач со сроками, сортировка по дедлайну, дни до срока, просроченные дела и отчёт строками со strftime — всё, что накопили девятнадцать уроков.
Редакция Питоники
Девятнадцать уроков собирали инструменты: объекты и форматы, разницы и сравнения, календарь, таблицы и журналы. Финальный проект складывает их в механизм, который понятно зачем существует: трекер дедлайнов. Дедлайн — это не строка в блокноте, а дата, с которой можно считать, — и весь проект это доказывает.
Постановка задачи
Трекер получает список дел со сроками и отдает хозяину то, ради чего его заводят:
- хранит задачи: название и срок — момент datetime, а не текст;
- сортирует дела по дедлайну: сначала то, что горит;
- считает дни до каждого срока — и отрицательные тоже;
- отличает просроченные дела и дела со сроком «сегодня»;
- печатает отчёт строками «25.10.2026 | дело | осталось 4 дн.».
Одно упрощение сразу договорим: «сегодня» в проекте — фиксированная переменная deadline_today = datetime(2026, 10, 21). В боевом коде она берётся из системы — этому был посвящён урок 3, — но пример с фиксированной датой печатает один и тот же отчёт при каждом запуске, и на нём можно проверять вывод. Все приёмы ниже к этому не привязаны: замените значение переменной — трекер продолжит работать.
from datetime import datetime
deadline_today = datetime(2026, 10, 21) # условное «сегодня» для примера
tasks = [
{'title': 'сдать отчёт по курсу', 'due': datetime(2026, 10, 25)},
{'title': 'заплатить за хостинг', 'due': datetime(2026, 10, 18)},
{'title': 'прочитать главу про timedelta', 'due': datetime(2026, 11, 2)},
{'title': 'продлить домен', 'due': datetime(2026, 10, 21)},
]
print(len(tasks))
print(tasks[0]['title'])
4 сдать отчёт по курсу
Четыре дела — намеренно с полным набором ситуаций: хостинг просрочен (18-е уже прошло), домен горит сегодня, отчёт через четыре дня, глава — в ноябре. Сроки — настоящие datetime из первого урока: с ними работает вычитание, сравнение и strftime, и это главный каркас проекта.
Шаг 1. Сортировка по дедлайну
Первое, что хочет видеть человек утром, — что горит первым. Список сортируется методом sort с ключом: lambda-функция достаёт из каждого словаря срок, и по нему даты выстраиваются сами:
from datetime import datetime
deadline_today = datetime(2026, 10, 21) # условное «сегодня» для примера
tasks = [
{'title': 'сдать отчёт по курсу', 'due': datetime(2026, 10, 25)},
{'title': 'заплатить за хостинг', 'due': datetime(2026, 10, 18)},
{'title': 'прочитать главу про timedelta', 'due': datetime(2026, 11, 2)},
{'title': 'продлить домен', 'due': datetime(2026, 10, 21)},
]
tasks.sort(key=lambda t: t['due'])
for t in tasks:
print(t['due'].strftime('%d.%m.%Y'), t['title'])
18.10.2026 заплатить за хостинг 21.10.2026 продлить домен 25.10.2026 сдать отчёт по курсу 02.11.2026 прочитать главу про timedelta
Список выстроился по срокам — от просроченного хостинга к ноябрьской главе. Ключ key=lambda t: t['due'] говорит сортировке, по чему сравнивать строки списка, а даты сравниваются сами — та же механика из урока про сравнение дат, что и у sorted по логу из урока 19. strftime печатает срок коротко, по-домашнему.
Шаг 2. Дни до дедлайна и просрочка
Теперь главное число проекта. Разность «срок минус сегодня» — timedelta из урока про разницы, и его поле days честно показывает даже просрочку:
from datetime import datetime
deadline_today = datetime(2026, 10, 21) # условное «сегодня» для примера
tasks = [
{'title': 'сдать отчёт по курсу', 'due': datetime(2026, 10, 25)},
{'title': 'заплатить за хостинг', 'due': datetime(2026, 10, 18)},
{'title': 'прочитать главу про timedelta', 'due': datetime(2026, 11, 2)},
{'title': 'продлить домен', 'due': datetime(2026, 10, 21)},
]
tasks.sort(key=lambda t: t['due'])
for t in tasks:
left = (t['due'] - deadline_today).days
print(t['title'], left)
заплатить за хостинг -3 продлить домен 0 сдать отчёт по курсу 4 прочитать главу про timedelta 12
Три числа — три статуса. Минус три у хостинга: срок наступил три дня назад, дело просрочено. Ноль у домена: разность дат с одинаковыми сутками — нулевой timedelta, срок именно сегодня. Положительные четыре и двенадцать — запас времени в днях. Вся шкала статусов вывелась из одного вычитания и знака числа.
Шаг 3. Отчёт строками
Осталось превратить числа в человеческий отчёт. Статус выбирается по знаку left, строка собирается strftime и f-строкой — форматирование из урока про strftime:
from datetime import datetime
deadline_today = datetime(2026, 10, 21) # условное «сегодня» для примера
tasks = [
{'title': 'сдать отчёт по курсу', 'due': datetime(2026, 10, 25)},
{'title': 'заплатить за хостинг', 'due': datetime(2026, 10, 18)},
{'title': 'прочитать главу про timedelta', 'due': datetime(2026, 11, 2)},
{'title': 'продлить домен', 'due': datetime(2026, 10, 21)},
]
tasks.sort(key=lambda t: t['due'])
print('Дела на', deadline_today.strftime('%d.%m.%Y'))
print('-' * 42)
for t in tasks:
left = (t['due'] - deadline_today).days
if left < 0:
status = f'просрочена на {-left} дн.'
elif left == 0:
status = 'срок сегодня'
else:
status = f'осталось {left} дн.'
print(f"{t['due'].strftime('%d.%m.%Y')} | {t['title']} | {status}")
Дела на 21.10.2026 ------------------------------------------ 18.10.2026 | заплатить за хостинг | просрочена на 3 дн. 21.10.2026 | продлить домен | срок сегодня 25.10.2026 | сдать отчёт по курсу | осталось 4 дн. 02.11.2026 | прочитать главу про timedelta | осталось 12 дн.
Вот и весь трекер: сортировка, вычитание, три ветки статуса и печать. Строка заголовка печатает «сегодня» тем же strftime, разделитель — умножение строки на 42, а в статусе просрочки стоит {-left}: минус на минус, чтобы человек увидел «на 3 дн.», а не «на -3 дн.». Скопируйте блок в свой проект, замените список дел — и трекер ваш.
from datetime import date, datetime
delta = date(2026, 10, 21) - datetime(2026, 10, 21)
print(delta)
TypeError: unsupported operand type(s) for -: 'datetime.date' and 'datetime.datetime'
Куда дальше
Трекер — точка выхода из раздела, но не из темы. Даты в объёме — это уже не список дел, а таблицы и базы: курс Pandas продолжит дело урока 18 — в финальном проекте на Pandas даты превратятся в тренды и сводные, а курс SQLAlchemy научит хранить даты в настоящей базе через типы DATE и TIMESTAMP — старт с первого урока SQLAlchemy. Логичный следующий шаг после datetime — Pandas: тот же модуль, но помноженный на тысячу строк.
Домашнее задание напоследок: добавьте в трекер приоритеты (сортируйте по паре «приоритет, срок») и счётчик просроченных дел — обе задачи решаются инструментами курса без новых тем.
Дедлайн — это не строка в блокноте, а дата, с которой можно считать.
Шаг 4. Счётчики: что горит, а что ждёт
Отчёт со строками читает человек, а решение принимает человек же — и ему нужен ответ на вопрос «сколько у меня горит». Отдельная выжимка из трёх чисел занимает пять строк и сразу показывает картину.
from datetime import datetime
today = datetime(2026, 10, 21)
tasks = [
{'title': 'сдать отчёт по курсу', 'due': datetime(2026, 10, 25)},
{'title': 'заплатить за хостинг', 'due': datetime(2026, 10, 18)},
{'title': 'прочитать главу про timedelta', 'due': datetime(2026, 11, 2)},
{'title': 'продлить домен', 'due': datetime(2026, 10, 21)},
]
tasks.sort(key=lambda t: t['due'])
overdue = [t for t in tasks if t['due'] < today]
soon = [t for t in tasks if t['due'] == today]
print(len(overdue), len(soon), len(tasks))
for t in overdue:
print(t['title'], (today - t['due']).days)
1 1 4 заплатить за хостинг 3
Одна просроченная задача, одна с сегодняшним сроком, всего четыре дела. У просроченной посчитали и величину — три дня. Заметьте, что для «просрочено» сравнение идёт через t['due'] < today, а не через разность со знаком: прямое сравнение дат читается прямо и не требует разбирать знак. Разность нужна для числа дней, сравнение — для флага.
Шаг 5. Приоритеты: сортировка по двум ключам
Реальный трекер умеет больше: не всё задачи одинаково важны, и при равных сроках первым должен идти более важный пункт. Ключ сортировки тогда перестаёт быть одной датой и становится кортежем из двух полей.
from datetime import datetime
tasks = [
{'title': 'отчёт', 'prio': 2, 'due': datetime(2026, 10, 21)},
{'title': 'глава', 'prio': 1, 'due': datetime(2026, 11, 2)},
{'title': 'хостинг', 'prio': 3, 'due': datetime(2026, 10, 18)},
]
tasks.sort(key=lambda t: (t['prio'], t['due']))
for t in tasks:
print(t['prio'], t['title'])
1 глава 2 отчёт 3 хостинг
Кортежи сравниваются поэлементно — ровно тот же приём, что в уроке про возраст, где сравнивались пары (месяц, день). Сначала сравнивается первый элемент — приоритет, и только при равенстве приоритетов в дело вступает второй — срок. Именно поэтому «глава» с приоритетом 1 оказалась раньше «отчёта» с приоритетом 2, хотя сроки у них разные и более ранним по времени был бы отчёт.
Шаг 6. Недели вместо дней
«Осталось 12 дней» звучит тревожнее, чем «осталось полторы недели», хотя это одно и то же. Для планирования недели удобнее, и пересчёт делается одним целочисленным делением.
from datetime import datetime
today = datetime(2026, 10, 21)
for t in (datetime(2026, 10, 25), datetime(2026, 11, 2)):
left = (t - today).days
print(left, left // 7, left % 7)
4 0 4 12 1 5
До отчёта 4 дня — это ноль полных недель и 4 дня остатка. До главы 12 дней — одна полная неделя и 5 дней. Обе величины выходят из одного деления на 7: целая часть — недели, остаток — дни. Это тот же приём, что и в уроке про разницы в днях, только вместо месяцев по 30 дней здесь недели по 7.
from datetime import datetime
from collections import defaultdict
today = datetime(2026, 10, 21)
tasks = [
{'title': 'отчёт', 'due': datetime(2026, 10, 25)},
{'title': 'глава', 'due': datetime(2026, 11, 2)},
{'title': 'подписка', 'due': datetime(2026, 10, 28)},
]
buckets = defaultdict(list)
for t in tasks:
buckets[(t['due'] - today).days // 7].append(t['title'])
for week in sorted(buckets):
print(week, buckets[week])
0 ['отчёт'] 1 ['глава', 'подписка']
Задачи собрались по корзинам: нулевая неделя — отчёт, первая — глава и подписка. defaultdict(list) из стандартной библиотеки создаёт пустой список для первого попадания в ключ, так что код не нужно готовить корзины заранее. Ключом служит целое число недель, поэтому корзины сортируются обычным sorted() — числа сравниваются между собой, а не как строки.
Шаг 7. Трекер в JSON
Последнее, что нужно живому трекеру, — обмен данными с внешним миром. И тут объект datetime мешает: json.dumps не умеет его сериализовать и падает с TypeError. Решение — превратить дату в строку на выходе, а на входе разобрать обратно.
import json
from datetime import datetime
today = datetime(2026, 10, 21)
tasks = [
{'title': 'отчёт', 'due': datetime(2026, 10, 25)},
]
payload = [
{'title': t['title'], 'due': t['due'].isoformat(), 'left': (t['due'] - today).days}
for t in tasks
]
print(json.dumps(payload, ensure_ascii=False))
[{"title": "отчёт", "due": "2026-10-25T00:00:00", "left": 4}]isoformat() даёт стандарт ISO 8601, который понимает любой другой язык и любая библиотека. Поле left считается при выгрузке и уезжает числом — получателю не нужно знать, что такое «сегодня» в вашем проекте. Флаг ensure_ascii=False оставляет русские буквы как есть: в JSON это законно, а читать такой файл глазами приятнее, чем разбирать \u043e\u0442\u0447\u0451\u0442.
import json
from datetime import datetime
raw = '[{"title": "отчёт", "due": "2026-10-25T00:00:00"}]'
data = json.loads(raw)
for item in data:
print(item['title'], datetime.fromisoformat(item['due']))
отчёт 2026-10-25 00:00:00
Круг замкнулся: fromisoformat разбирает строку обратно в объект datetime, и трекер получает даты, с которыми снова можно считать. Между этими двумя точками живёт и format='%d.%m.%Y' для произвольных форматов, и fromtimestamp для unix-времени из урока 13 — три разных входа в одни и те же данные. Выбор формата определяется тем, что вы получаете: свой формат, unix-время или ISO.
from datetime import datetime
today = datetime(2026, 10, 21)
tasks = [
{'title': 'отчёт', 'due': datetime(2026, 10, 25)},
{'title': 'хостинг', 'due': datetime(2026, 10, 18)},
]
assert (tasks[0]['due'] - today).days == 4
assert (tasks[1]['due'] - today).days == -3
assert tasks[0]['due'] > tasks[1]['due']
print('проверки прошли')
проверки прошли
Три assert на весь трекер. Пока «сегодня» фиксировано, эти проверки дают один и тот же результат при каждом запуске — и в тестах, и в песочнице курса. Мы добавили такие проверки в реальный проект после того, как отчёт однажды показал «осталось 2 дня» вместо «осталось 3».
| Возможность трекера | Инструмент курса | Сложность |
|---|---|---|
| Сортировка по сроку | tasks.sort(key=lambda t: t['due']) | одна строка |
| Сортировка по приоритету и сроку | tasks.sort(key=lambda t: (t['prio'], t['due'])) | кортеж-ключ |
| Дни до срока | (t['due'] - deadline_today).days | вычитание |
| Просроченные дела | [t for t in tasks if t['due'] < deadline_today] | список |
| Формат отчёта | strftime('%d.%m.%Y') и f-строка | склейка |
| Выгрузка в JSON | isoformat() и fromisoformat() | пара вызовов |
| Проверка расчётов | assert на фиксированной дате | одна строка на проверку |
Семь возможностей и инструмент под каждую — из тех девятнадцати уроков, что были до этого. Ни одна из них не требует сторонних библиотек: datetime, json, collections и встроенные методы списков закрывают весь трекер. Дальше начинается другая история — когда дел становится тысяча, а не четыре, и появляются таблицы, группировки и временные ряды.
Что именно стоит заменить, когда трекер переезжает в боевой режим: «сегодня» становится системным моментом, задачи переезжают в базу, а вместо списка словарей появляется таблица pandas с колонками «название», «срок», «приоритет». Логика отчёта при этом не меняется ни на строчку — она уже написана в терминах дат, а не в терминах списка. Именно ради этого трекер и собирался на чистом datetime: переезд должен быть дешёвым.
Сначала предскажи ответ в голове — это главный навык программиста.
from datetime import datetime
deadline_today = datetime(2026, 10, 21)
due = datetime(2026, 10, 18)
left = (due - deadline_today).days
print(left, left < 0)
from datetime import datetime
d = datetime(2026, 11, 2, 18, 30)
print(d.strftime('%d.%m.%Y %H:%M'))
from datetime import datetime
tasks = [
('отчёт', datetime(2026, 10, 25)),
('домен', datetime(2026, 10, 21)),
]
first = min(tasks, key=lambda t: t[1])
print(first[0])
from datetime import datetime
today = datetime(2026, 10, 21)
tasks = [
{'title': 'отчёт', 'due': datetime(2026, 10, 25)},
{'title': 'хостинг', 'due': datetime(2026, 10, 18)},
]
print(len([t for t in tasks if t['due'] < today]))
from datetime import datetime
tasks = [
{'title': 'отчёт', 'prio': 2, 'due': datetime(2026, 10, 21)},
{'title': 'глава', 'prio': 1, 'due': datetime(2026, 11, 2)},
]
tasks.sort(key=lambda t: (t['prio'], t['due']))
print([t['title'] for t in tasks])
1. В каком виде трекер из проекта хранит срок задачи?
2. Почему tasks.sort(key=lambda t: t['due']) сортирует дела правильно?
3. left = (t['due'] - deadline_today).days равно -3. Что это значит?
4. Зачем в примерах проекта фиксированное deadline_today = datetime(2026, 10, 21)?
5. Что напечатает t['due'].strftime('%d.%m.%Y') для срока datetime(2026, 10, 25)?
6. Как отсортировать задачи сначала по приоритету, а при равенстве — по сроку?
7. Почему json.dumps падает на трекере, где срок хранится объектом datetime?
8. Что даст left // 7 и left % 7 для значения left = 12?
9. Зачем в трекере фиксированная deadline_today вместо datetime.now()?
Допишите трекер: добавьте в список задач дело «оформить подписку» со сроком 9 ноября 2026 года, отсортируйте задачи по сроку и напечатайте отчёт тем же форматом, что в проекте: дата, вертикальная черта, название, вертикальная черта, статус. Статусы: просрочка, «срок сегодня», «осталось N дн.».
Как сделать трекер задач с дедлайнами на Python?
Храните задачи списком словарей с полями «название» и «срок» — срок как datetime. Сортируйте по сроку методом sort с ключом key=lambda t: t['due'], считайте дни до дедлайна выражением (t['due'] - deadline_today).days и печатайте отчёт строками со strftime. Полная версия — финальный проект курса datetime, все блоки которого запускаются прямо на странице.
Урок 20: проект трекер дедлайновКак найти просроченные задачи в Python?
Сравните срок с «сегодня»: задача просрочена, если (t['due'] - deadline_today).days < 0 или, что то же самое, t['due'] < deadline_today. Для списка отбор делается списочным включением [t for t in tasks if t['due'] < deadline_today], а величину просрочки даёт то же вычитание со знаком минус.
Почему в проекте используется фиксированная дата вместо текущей?
Примеры курса обязаны печатать один и тот же вывод при любом запуске — иначе его не проверить. Фиксированная переменная deadline_today = datetime(2026, 10, 21) делает отчёт детерминированным; это же правило упрощает тесты: проверки не зависят от дня запуска. В боевом коде на её месте стоит системный момент.
Что учить после курса datetime?
Даты в реальных приложениях живут в таблицах и базах. Курс Pandas продолжит урок про to_datetime: временные ряды, группировки по месяцам и финальный проект с трендами. Курс SQLAlchemy покажет, как даты хранятся в базах через типы DATE и TIMESTAMP. Логичный порядок: datetime, затем Pandas, затем SQLAlchemy.
Финальный проект PandasКак добавить в трекер сортировку по приоритету?
Сделайте ключ сортировки кортежем: tasks.sort(key=lambda t: (t['prio'], t['due'])). Кортежи сравниваются поэлементно — сначала приоритет, и только при равенстве срок. Важно помнить, что порядок полей задаёт смысл: (prio, due) сортирует по важности, (due, prio) — по срокам.
Урок 20: проект трекер дедлайновКак выгрузить трекер в JSON?
Объект datetime не сериализуется: json.dumps бросит TypeError. Превратите дату в строку через isoformat() при выгрузке и разберите обратно через datetime.fromisoformat() на входе. Поле с числом дней посчитайте заранее и положите рядом — получателю не нужно знать, что такое «сегодня» в вашем проекте.
Как проверить, что трекер считает дни правильно?
Через assert на фиксированной дате: (due - today).days == 4 для срока через четыре дня и == -3 для просроченного на три дня. Такие проверки дают один и тот же результат при каждом запуске и ловят ошибку на единицу — ровно ту, что не видно в коде.
Что делать, если в трекере смешались date и datetime?
Вычитание date из datetime падает с TypeError: unsupported operand type(s). Держите в проекте один тип — мы выбрали datetime, потому что в сроках может быть время. Если данные пришли как date, приведите их через datetime.combine(d, time(0, 0)) — момент получится на полночь выбранного дня.
Понравился урок? Сошлитесь на него
«Дедлайн — это не строка в блокноте, а дата, с которой можно считать.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Datetime · Урок 19
Даты в логах: разбор реального формата
Журнал из строк вида «2026-10-21 09:15:03 ERROR …»: разбираем момент через strptime, сортируем события по времени и вырезаем записи за интервал.
Pandas · Урок 10
Проект на Pandas: полный анализ продаж интернет-магазина
Финальный проект раздела: сырая выгрузка заказов, чистка по чек-листу, выручка и средний чек, топ-товары, сводные по городам, тренд по месяцам и выводы для руководителя.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
sqlite3 · Урок 10
Проект: база книжной библиотеки на SQLite с отчётами SQL
Финальный проект курса: база библиотеки из трёх связанных таблиц — авторы, книги, выдачи — и четыре настоящих отчёта на SQL. Собираем всё, что выучили за девять уроков.
sqlite python проектsql проект с нуля
json · Урок 20
Проект: каталог книг — от URL до файла
Финал раздела: один скрипт собирает каталог книг — параметры в URL, разбор ответа, фильтр по рейтингу, сортировка по цене и проверенный файл на диске.
python проект json api каталогpython проект пример для начинающих
Datetime · Урок 11
combine и replace: datetime из частей
date знает день, time знает часы — combine склеивает их в datetime, а replace меняет отдельные поля и возвращает новый объект: datetime неизменяем.
datetime combine replacedatetime combine python