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

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

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

Рабочие дни: будни без выходных

Перебираем даты циклом с timedelta, отсекаем выходные условием weekday() < 5 и отвечаем на два вопроса планировщика: сколько будней между датами и на какое число выпадает срок через N рабочих дней.

Редакция Питоники

Возраст и дни до события из прошлого урока считают все сутки подряд. Но планировщик живёт другими единицами: «срок через семь рабочих дней», «в месяце двадцать два рабочих дня». Календарь из урока про calendar таких чисел тоже не печатает. Хорошая новость: для базовой версии хватает двух вещей, которые уже знакомы — номеров дня недели из урока про weekday и шага timedelta из урока про разницы.

Фильтр будней: weekday() < 5

У метода weekday() есть удобное свойство: он считает с понедельника, то есть все рабочие дни — это номера от 0 до 4, а суббота и воскресенье — 5 и 6. Одно сравнение отделяет будни от выходных. Пройдём циклом неделю 19–25 октября 2026 и соберём только будни:

неделя глазами планировщика
from datetime import date, timedelta

d = date(2026, 10, 19)   # понедельник
end = date(2026, 10, 25)  # воскресенье

workdays = []
while d <= end:
    if d.weekday() < 5:
        workdays.append(d)
    d += timedelta(days=1)

print(len(workdays))
print(workdays[0])
print(workdays[-1])
Вывод
5
2026-10-19
2026-10-23

Из семи дней недели в список попали пять: с понедельника 19-го по пятницу 23-е. Обратите внимание на шаг цикла: d += timedelta(days=1) не меняет дату на месте — объекты дат неизменяемы, — а привязывает к имени d новую дату на день позже. Это тот же приём replace из прошлого урока: исходник цел, переменная смотрит на следующее значение.

d.weekday()День неделиРабочий?
0понедельникда
1вторникда
2средада
3четвергда
4пятницада
5субботанет
6воскресеньенет

Сколько рабочих дней между датами

Соберём цикл в функцию — она пригодится и для отчётов, и для следующего вопроса. Границы считаем включённо: и первый, и последний день участвуют, если не выходные.

будни между датами, включая границы
from datetime import date, timedelta

def count_workdays(start, end):
    days = 0
    d = start
    while d <= end:
        if d.weekday() < 5:
            days += 1
        d += timedelta(days=1)
    return days

print(count_workdays(date(2026, 10, 1), date(2026, 10, 31)))
print(count_workdays(date(2026, 10, 17), date(2026, 10, 19)))
print(count_workdays(date(2026, 10, 24), date(2026, 10, 25)))
Вывод
22
1
0

В октябре 2026 года 22 будня — стандартный полный месяц. Диапазон с 17 по 19 октября даёт единицу: суббота и воскресенье отсеялись, остался только понедельник. А диапазон 24–25 октября — ровно выходные, ноль рабочих дней. Три числа проверяют функцию с трёх сторон: полный месяц, диапазон с выходными внутри и диапазон из одних выходных.

Дата через N рабочих дней

Обратная задача планировщика: не посчитать будни в периоде, а найти дату, когда сгорит срок — «принять через семь рабочих дней». Логика зеркальная: шагаем по одному дню, но счётчик уменьшаем только на буднях.

срок через N рабочих дней
from datetime import date, timedelta

def add_workdays(start, n):
    d = start
    while n > 0:
        d += timedelta(days=1)
        if d.weekday() < 5:
            n -= 1
    return d

print(add_workdays(date(2026, 10, 21), 1))
print(add_workdays(date(2026, 10, 21), 7))
Вывод
2026-10-22
2026-10-30

Старт 21 октября 2026 — среда. Один рабочий день спустя — четверг 22-е. А семь рабочих дней — это пятница 30-го: цикл перешагнул одни выходные 24–25 октября, потому что суббота и воскресенье шагают, но счётчик не уменьшают. Сам стартовый день не считается — отсчёт начинается со следующего дня, как и ожидает заказчик от фразы «через семь рабочих дней».

встречная проверка: семь вышло — семь внутри
from datetime import date, timedelta

def count_workdays(start, end):
    days = 0
    d = start
    while d <= end:
        if d.weekday() < 5:
            days += 1
        d += timedelta(days=1)
    return days

def add_workdays(start, n):
    d = start
    while n > 0:
        d += timedelta(days=1)
        if d.weekday() < 5:
            n -= 1
    return d

start = date(2026, 10, 21)
finish = add_workdays(start, 7)
print(finish)
print(count_workdays(start, finish - timedelta(days=1)))
Вывод
2026-10-30
7

Что дальше

Одна дата за раз — это ручной труд. Когда дат становятся десятки и они приезжают таблицей, циклы заменяет pandas: следующий урок — даты в Pandas через to_datetime, где столбец строк одним вызовом превращается в настоящие даты, а фильтры по периоду становятся сравнениями.

Условие d.weekday() < 5 честно работает до первого производственного календаря: праздники оно не знает и не угадает.

Границы периода решают всё

Прежде чем считать дальше, надо договориться о границах. Функция count_workdays считает и стартовый, и конечный день — если они будние. Для «сколько рабочих дней было в октябре» это верно: период включает обе даты. Но для «сколько рабочих дней осталось до дедлайна» разумнее считать дни строго после сегодня: иначе сегодняшний день попадёт в счёт и срок придёт на день раньше.

одна и та же неделя, две рамки
from datetime import date, timedelta

start = date(2026, 10, 26)
end = date(2026, 10, 30)

d = start
while d <= end:
    print(d.isoformat(), d.strftime('%a'), d.weekday())
    d += timedelta(days=1)
Вывод
2026-10-26 Mon 0
2026-10-27 Tue 1
2026-10-28 Wed 2
2026-10-29 Thu 3
2026-10-30 Fri 4

Пять дней подряд, с понедельника по пятницу, номера от 0 до 4 — ровно то, что отсекает условие weekday() < 5. Заметьте, что strftime с шаблоном %a даёт английские сокращения: Monday, Tuesday. Русские названия дней недели в datetime живут отдельным словарём, о нём шла речь в уроке про дни недели. Числа в этом виде уже пригодны для выгрузки в CSV или JSON — и, в отличие от объектов date, они сериализуются без плясок.

Производственный календарь: свой справочник

Праздники — это не проблема языка, а отсутствие данных. Но данные найти проще, чем кажется: список нерабочих дней конкретного года — это просто множество дат, с которым надо сверяться. Добавив его в условие, мы получаем почти настоящий производственный календарь.

ноябрь с одним праздником
from datetime import date, timedelta

holidays = {date(2026, 11, 4)}

def count_workdays(start, end):
    days = 0
    d = start
    while d <= end:
        if d.weekday() < 5 and d not in holidays:
            days += 1
        d += timedelta(days=1)
    return days

print(count_workdays(date(2026, 11, 1), date(2026, 11, 30)))
Вывод
20

Ноябрь 2026 года без праздников дал бы 21 рабочий день, а с 4 ноября — государственным выходным — получается 20. Проверка d not in holidays стоит ровно столько же, сколько weekday() < 5, и не ломает ничего: множество дат ищется за O(1), а сам цикл по-прежнему идёт по дням.

рабочий день — одна проверка
from datetime import date

holidays = {date(2026, 11, 4)}

def is_business_day(d):
    return d.weekday() < 5 and d not in holidays

for d in (date(2026, 11, 3), date(2026, 11, 4), date(2026, 11, 7)):
    print(d.isoformat(), is_business_day(d))
Вывод
2026-11-03 True
2026-11-04 False
2026-11-07 False

Вынос проверки в отдельную функцию решает сразу две задачи. Во-первых, условие перестаёт дублироваться: и подсчёт, и добавление рабочих дней пользуются одним и тем же определением, и разойтись они не могут. Во-вторых, появляется место для будущих правил — скажем, для переносов: правило добавляется в тело функции, а не в десять мест по коду.

Оптимизация, которая обычно не нужна

Цикл из урока шагает по одному дню и на выходных просто не уменьшает счётчик. За год — 365 итераций, за десять лет — 3650. Это ничто: на нашем ноутбуке такой подсчёт укладывается в десятки микросекунд, и заметить разницу можно только бенчмарком. Но если очень хочется, выходные можно перепрыгивать целиком.

перепрыгивание выходных
from datetime import date, timedelta

def add_workdays_fast(start, n):
    d = start
    while n > 0:
        d += timedelta(days=1)
        if d.weekday() == 5:
            d += timedelta(days=2)
            n -= 1
        elif d.weekday() == 6:
            d += timedelta(days=1)
            n -= 1
        else:
            n -= 1
    return d

def add_workdays_plain(start, n):
    d = start
    while n > 0:
        d += timedelta(days=1)
        if d.weekday() < 5:
            n -= 1
    return d

for n in (1, 5, 7, 20):
    print(n, add_workdays_fast(date(2026, 10, 21), n), add_workdays_plain(date(2026, 10, 21), n))
Вывод
1 2026-10-22 2026-10-22
5 2026-10-28 2026-10-28
7 2026-10-30 2026-10-30
20 2026-11-18 2026-11-18

Результаты совпадают: и быстрый вариант, и простой дают одно и то же. Разница только в числе итераций — примерно вдвое меньше, потому что суббота и воскресенье пропускаются за один шаг. Мы оставили в проекте простой вариант: он читается как урок и его можно проверить глазами, а оптимизация без замеров — это риск без выгоды. Если бы период считался в цикле по миллиону задач, тогда уже стоило бы смотреть в сторону pandas — но не раньше.

ЗадачаФункция из урокаЧто считает
Сколько будней в периодеcount_workdays(start, end)с обоим концом, если дни будние
Дата через N рабочих днейadd_workdays(start, n)стартовый день не входит в отсчёт
Рабочий ли конкретный деньd.weekday() < 5только суббота и воскресенье
С учётом праздниковd.weekday() < 5 and d not in holidaysплюс свой справочник дат

Четыре строки, которые закрывают планировщик целиком. Самая частая ошибка при переносе этой логики в свой проект — разные соглашения о стартовом дне: одна функция считает «через один рабочий день» как завтра, другая как сегодня же. Проверяется это просто: посчитайте через add_workdays, а потом обратно через count_workdays, и числа должны сойтись.

взаимная проверка через границу
from datetime import date, timedelta

def count_workdays(start, end):
    days = 0
    d = start
    while d <= end:
        if d.weekday() < 5:
            days += 1
        d += timedelta(days=1)
    return days

def add_workdays(start, n):
    d = start
    while n > 0:
        d += timedelta(days=1)
        if d.weekday() < 5:
            n -= 1
    return d

start = date(2026, 10, 21)
finish = add_workdays(start, 12)

print(finish)
print(count_workdays(start + timedelta(days=1), finish))
Вывод
2026-11-06
12

Здесь граница сдвинута на день: стартовый день не входит в отсчёт, поэтому при обратной проверке мы начинаем со следующего дня. Если этого не сделать, получится 13 вместо 12 — тот самый сдвиг на единицу, о котором шла речь в питфолле выше. Взаимная проверка занимает четыре строки и ловит ровно те ошибки, которые не видны в коде.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

from datetime import date

d = date(2026, 10, 24)
print(d.weekday() < 5)
from datetime import date, timedelta

d = date(2026, 10, 23)
d += timedelta(days=3)
print(d, d.weekday())
from datetime import date, timedelta

d = date(2026, 10, 21)
step = timedelta(days=1)
found = 0
while found < 2:
    d += step
    if d.weekday() < 5:
        found += 1
print(d)
from datetime import date

holidays = {date(2026, 11, 4)}

d = date(2026, 11, 4)
print(d.weekday() < 5)
print(d in holidays)
from datetime import date, timedelta

def add_workdays(start, n):
    d = start
    while n > 0:
        d += timedelta(days=1)
        if d.weekday() < 5:
            n -= 1
    return d

print(add_workdays(date(2026, 10, 21), 5))
print(add_workdays(date(2026, 10, 24), 1))
Проверь себя
0 / 9

1. Какой номер у субботы в системе d.weekday()?

2. Что вернёт count_workdays(date(2026, 10, 24), date(2026, 10, 25))?

3. Что вернёт add_workdays(start, 0) из урока?

4. Почему в цикле пишут d += timedelta(days=1), а не d + timedelta(days=1)?

5. Функция count_workdays из урока посчитала 22 будня в октябре 2026. Учитывает ли она государственные праздники?

6. Как учесть в счётчике рабочих дней государственный праздник?

7. Что даст проверка count_workdays(start + timedelta(days=1), finish) вместо count_workdays(start, finish)?

8. Стоит ли оптимизировать цикл подсчёта рабочих дней перепрыгиванием выходных?

9. Что произойдёт при add_workdays(date(2026, 10, 24), 1), где 24 октября — суббота?

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

Заказ просрочен, если его не исполнить за N рабочих дней. Старт — пятница 6 ноября 2026 года. Напишите функцию add_workdays(start, n) из урока и выведите два срока: через 3 рабочих дня и через 4 рабочих дня от этой даты. Стартовый день в отсчёт не входит.

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

Как посчитать количество рабочих дней между датами в Python?

Пройдите циклом от начальной до конечной даты с шагом timedelta(days=1) и увеличивайте счётчик, только если d.weekday() < 5 — так отсекаются суббота (5) и воскресенье (6). Границы считаются включённо, если сами не выходные. В октябре 2026 года такой подсчёт даёт 22 рабочих дня.

Урок 17: рабочие дни в Python

Как получить дату через N рабочих дней в Python?

Шагайте по календарю по одному дню и уменьшайте счётчик N только на буднях: d += timedelta(days=1), затем if d.weekday() < 5: n -= 1. Когда счётчик исчерпан, в d лежит искомая дата. Семь рабочих дней от среды 21 октября 2026 — это пятница 30-е: выходные цикл перешагивает.

Учитывает ли weekday() государственные праздники?

Нет. weekday() знает только положение дня в неделе, поэтому 4 ноября 2026 (вторник) он назовёт рабочим днём. Для учёта праздников и переносов используют сторонние библиотеки с производственными календарями стран либо собственный справочник праздничных дат, который добавляется к условию weekday() < 5.

Считается ли стартовый день в «через N рабочих дней»?

В реализации урока — нет: add_workdays(start, 1) возвращает следующий будень. Но регламенты бывают разные, и расхождение на единицу — самый частый спор про сроки. Зафиксируйте правило в описании функции: если стартовый день входит в отсчёт, уменьшайте счётчик ещё и за сам старт, когда он будний.

Как учесть в Python государственные праздники при подсчёте рабочих дней?

Соберите множество нерабочих дат: holidays = {date(2026, 11, 4)}, и проверяйте оба условия — d.weekday() < 5 and d not in holidays. Для ноября 2026 года счёт даст 20 рабочих дней вместо 21. Переносы выходных такой справочник не покрывает: для них нужен настоящий производственный календарь.

Урок 17: рабочие дни

Сколько рабочих дней было в 2026 году?

Подсчёт циклом по всем датам года с условием weekday() < 5 даёт 261 рабочий день. Проверяется это функцией count_workdays(date(2026, 1, 1), date(2026, 12, 31)) — цикл делает 365 шагов, что занимает ничтожное время.

Как найти первый понедельник месяца в Python?

Возьмите первое число месяца и шагайте вперёд, пока weekday() не станет равен 0: d = date(year, month, 1); while d.weekday() != 0: d += timedelta(days=1). Для октября 2026 года первый понедельник — 5 октября. Тот же приём через calendar.monthcalendar даёт недели месяца списками.

Урок 15: модуль calendar

Понравился урок? Сошлитесь на него

«Условие d.weekday() < 5 честно работает до первого производственного календаря: праздники оно не знает и не угадает.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

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

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

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