Рабочие дни: будни без выходных
Перебираем даты циклом с 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 рабочих дней
Обратная задача планировщика: не посчитать будни в периоде, а найти дату, когда сгорит срок — «принять через семь рабочих дней». Логика зеркальная: шагаем по одному дню, но счётчик уменьшаем только на буднях.
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))
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 октября — суббота?
Заказ просрочен, если его не исполнить за N рабочих дней. Старт — пятница 6 ноября 2026 года. Напишите функцию add_workdays(start, n) из урока и выведите два срока: через 3 рабочих дня и через 4 рабочих дня от этой даты. Стартовый день в отсчёт не входит.
Как посчитать количество рабочих дней между датами в 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-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Datetime · Урок 16
Возраст и разница в днях
Полный возраст из даты рождения с учётом того, был ли день рождения в этом году, дни до события и недели из timedelta — три расчёта, которые нужны в любом проекте с датами.
Datetime · Урок 18
Даты в Pandas: to_datetime
Столбец дат приезжает из выгрузки строками — pd.to_datetime делает его настоящими датами, после чего работают dt.year, dt.month, фильтры по периоду и сортировка по времени.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
openpyxl · Урок 9
Даты в Excel через openpyxl: запись, формат и арифметика
Пишем в ячейку настоящий datetime, показываем его по-русски маской DD.MM.YYYY и считаем дни между датами — а заодно узнаём, почему дату из файла нельзя сравнивать со строкой.
openpyxl дата в ячейкеexcel python дата
Исключения · Урок 16
Ошибки в цикле Python: try внутри for и продолжение после сбоя
Сто строк данных, одна битая — и без защиты падает весь проход. Ставим try вокруг одного элемента: сбой обрабатывается, цикл идёт дальше, сбои считаются и попадают в журнал.
python продолжить цикл после ошибкипропустить ошибку python
Datetime · Урок 6
timedelta: разница между моментами
«Срок подписки истекает через 7 дней», «событие было 3 часа назад» — за этим стоит timedelta. Вычитаем моменты, читаем дни и секунды, прибавляем разницу к датам.
timedelta pythonpython timedelta примеры