Twice a month I need the same thing: the list of actual working days in a month, to spread hours over, or the answer to "how many workdays until the 15th?". The first time, I went looking for a library. Then I noticed the entire problem is a loop plus one quirk of the standard datetime module.
from datetime import date, timedelta
d = date(2026, 9, 1)
while d <= date(2026, 9, 30):
print(d)
d += timedelta(days=1)
That's the whole engine. A date plus a timedelta gives you the next date, and Python does every piece of calendar math for you — month lengths, leap years, all of it. You just walk. Everything else in this post is this loop wearing different clothes.
Weekday numbers. d.weekday() returns 0 for Monday and 6 for Sunday, so a day counts as a workday when d.weekday() < 5:
date(2026, 9, 1).weekday() # 1 — a Tuesday. Monday is 0.
I have written > 5 here at least once, which silently counts Saturdays as workdays, and shipped it — because my test dates kept landing on Mondays.
from datetime import date, timedelta
def business_days(year, month):
first = date(year, month, 1)
if month == 12:
next_month = date(year + 1, 1, 1)
else:
next_month = date(year, month + 1, 1)
last = next_month - timedelta(days=1)
return [first + timedelta(days=n)
for n in range((last - first).days + 1)
if (first + timedelta(days=n)).weekday() < 5]
days = business_days(2026, 9)
print(len(days)) # 22
print(days[0]) # 2026-09-01
print(days[-1]) # 2026-09-30
The trick worth stealing is the last day of the month: take the first of the next month and subtract one day. December is the only month that needs the year bumped, which is what the if is for. September 2026 has 22 business days, and I didn't count that by hand — the code did.
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, 9, 1), 10)) # 2026-09-15
Ten workdays after Tuesday the 1st is Tuesday the 15th — the two weekends in between simply never counted. Delivery estimates, "one month of support from Friday", invoice due dates: same function.
The month's timesheet, generated from the same list:
days = business_days(2026, 9)
print(f"Workdays in September 2026: {len(days)}\n")
for d in days:
if d.weekday() == 0: # Monday: gap between weeks
print()
print(f" {d:%a %d %b} ______ h")
f-string format specs work on dates, so {d:%a %d %b} gives Tue 01 Sep. The output is one line per workday — 22 of them — with a blank line before each Monday so the weeks read as blocks. I print it at the start of the month and fill it in by hand, which is honestly the entire reason the function exists.
I'd rather own these fifteen lines, which I can re-read in a minute, than install a dependency that promises to handle every calendar quirk. Holidays are the actual hard part of workday math, and no library guesses my company's bridge days around Christmas right either — those I pass in by hand when they matter. For everything else, a loop and weekday() have been enough for years.