Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 20 из 20 Начальный 25 мин 150 XP

Проект: трекер дедлайнов

Финальный проект раздела: список задач со сроками, сортировка по дедлайну, дни до срока, просроченные дела и отчёт строками со 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'
Блок намеренно не запускается: показана последняя строка traceback при вычитании datetime из date — питфолл из врезки выше.

Куда дальше

Трекер — точка выхода из раздела, но не из темы. Даты в объёме — это уже не список дел, а таблицы и базы: курс 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. Решение — превратить дату в строку на выходе, а на входе разобрать обратно.

выгрузка в JSON
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.

обратно из JSON
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.

проверки assert
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-строкасклейка
Выгрузка в JSONisoformat() и 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])
Проверь себя
0 / 9

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()?

Карточки терминов
Запомнено: 0 / 11
Практика

Допишите трекер: добавьте в список задач дело «оформить подписку» со сроком 9 ноября 2026 года, отсортируйте задачи по сроку и напечатайте отчёт тем же форматом, что в проекте: дата, вертикальная черта, название, вертикальная черта, статус. Статусы: просрочка, «срок сегодня», «осталось N дн.».

practice.py
Вопросы и ответы по уроку

Как сделать трекер задач с дедлайнами на 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.