Workday calculations with a built-in Norwegian calendar
Project description
pyworkdates
Dependency-free workday calculations for Python, with a built-in Norwegian calendar.
pyworkdates answers questions like is this date a workday?, what date is
three workdays from now?, and which workdays fall in this range? — using
only the Python standard library. It ships with a built-in Norwegian holiday
calendar, and every function accepts a calendar argument to override it with
any collection of dates.
Installation
pip install pyworkdates
or with uv:
uv add pyworkdates
Quickstart
Using the built-in Norwegian calendar (the default when no calendar is
given):
from pyworkdates import add_workdays, is_workday, next_workday
print(is_workday("2026-07-06")) # True (a regular Monday)
print(is_workday("2026-05-01")) # False (a holiday in the built-in calendar)
print(is_workday("2026-07-11")) # False (a Saturday)
print(add_workdays("2026-07-06", 3)) # 2026-07-09
print(next_workday("2026-07-10")) # 2026-07-13 (Friday -> Monday)
Overriding the calendar per call — pass any collection of dates:
from pyworkdates import add_workdays, is_workday
us_holidays = ["2026-07-03", "2026-11-26", "2026-12-25"] # a few US 2026 holidays
print(is_workday("2026-07-03", calendar=us_holidays)) # False
print(is_workday("2026-05-01", calendar=us_holidays)) # True (not in this calendar)
print(add_workdays("2026-07-02", 1, calendar=us_holidays)) # 2026-07-06
Defining a calendar once and reusing it with WorkdayCalendar:
from pyworkdates import WorkdayCalendar
us = WorkdayCalendar(["2026-07-03", "2026-11-26", "2026-12-25"])
print(us.is_workday("2026-07-03")) # False
print(us.next_workday("2026-11-25")) # 2026-11-27
print(us.workdays_between("2026-12-24", "2026-12-28"))
# [datetime.date(2026, 12, 24), datetime.date(2026, 12, 28)]
Key concepts
-
Workday: a day that is neither a weekend day (Saturday/Sunday) nor a date in the active calendar.
-
Built-in calendar: Norwegian holidays, hardcoded for the years 2016–2050. It is the active calendar whenever
calendaris not given. -
Coverage guard: any computation that touches a date outside 2016–2050 while using the built-in calendar raises
ValueError— never a silent wrong answer. Pass an explicitcalendarto work outside that range:>>> from pyworkdates import is_workday >>> is_workday("2055-01-04") Traceback (most recent call last): ... ValueError: Date 2055-01-04 is outside the built-in calendar's coverage (2016-2050). Pass an explicit 'calendar' argument to work with dates outside this range. >>> is_workday("2055-01-04", calendar=["2055-01-01"]) True
-
Custom calendars: any iterable of
datetime.date,datetime.datetime, or ISO strings"YYYY-MM-DD", freely mixed. Custom calendars are trusted as-is and never range-checked. -
Input and output types: every function accepts a
datetime.date, adatetime.datetime(its date part is used), or an ISO string"YYYY-MM-DD". Date-returning functions always returndatetime.date. Invalid input raisesValueError.
API reference
Every function takes an optional calendar keyword argument: None (the
default) means the built-in Norwegian calendar; otherwise the supplied
calendar is used. All examples below are doctest-verified.
is_workday(date, calendar=None) -> bool
True if the date is a workday: neither a weekend day nor a date in the active calendar.
>>> from pyworkdates import is_workday
>>> is_workday("2026-07-06") # a regular Monday
True
>>> is_workday("2026-05-01") # a holiday in the built-in calendar
False
is_holiday(date, calendar=None) -> bool
True if the date is in the active calendar. Weekend days are not holidays unless the calendar contains them.
>>> from pyworkdates import is_holiday
>>> is_holiday("2026-05-01")
True
>>> is_holiday("2026-07-11") # a Saturday, but not in the calendar
False
add_workdays(date, n, calendar=None) -> datetime.date
The date moved n workdays forward, or backward for negative n. Starting
from a non-workday does not first roll to one: Saturday plus one workday is
Monday, and Saturday minus one workday is Friday (holidays permitting). For
n=0 the date is returned unchanged if it is a workday, otherwise it rolls
forward to the nearest workday. n must be an int; bool and non-int
values raise ValueError.
>>> from pyworkdates import add_workdays
>>> add_workdays("2026-07-06", 3) # Monday + 3 workdays
datetime.date(2026, 7, 9)
>>> add_workdays("2026-07-06", -1) # Monday - 1 workday
datetime.date(2026, 7, 3)
>>> add_workdays("2026-07-11", 0) # Saturday rolls forward to Monday
datetime.date(2026, 7, 13)
next_workday(date, calendar=None) -> datetime.date
The first workday strictly after the date. The result is always later than
date, even when date itself is a workday.
>>> from pyworkdates import next_workday
>>> next_workday("2026-07-06") # a Monday: strictly after
datetime.date(2026, 7, 7)
>>> next_workday("2026-04-01") # Easter holidays start the next day
datetime.date(2026, 4, 7)
previous_workday(date, calendar=None) -> datetime.date
The first workday strictly before the date. The result is always earlier than
date, even when date itself is a workday.
>>> from pyworkdates import previous_workday
>>> previous_workday("2026-07-07") # a Tuesday: strictly before
datetime.date(2026, 7, 6)
>>> previous_workday("2027-01-04") # skips New Year's Day and the weekend
datetime.date(2026, 12, 31)
first_workday_of_month(date, calendar=None) -> datetime.date
The first workday of the date's month. Only days within that month are
examined. If the month contains no workdays — possible only with a custom
calendar — a ValueError is raised.
>>> from pyworkdates import first_workday_of_month
>>> first_workday_of_month("2026-05-20") # May 1st is a Friday holiday
datetime.date(2026, 5, 4)
>>> every_day_off = [f"2026-02-{day:02d}" for day in range(1, 29)]
>>> first_workday_of_month("2026-02-10", calendar=every_day_off)
Traceback (most recent call last):
...
ValueError: Month 2026-02 contains no workdays with the active calendar.
last_workday_of_month(date, calendar=None) -> datetime.date
The last workday of the date's month. Only days within that month are
examined, and the same "month contains no workdays" ValueError applies.
>>> from pyworkdates import last_workday_of_month
>>> last_workday_of_month("2026-05-15") # May 31st 2026 is a Sunday
datetime.date(2026, 5, 29)
last_workday_of_previous_month(date, calendar=None) -> datetime.date
The last workday of the month before the date's month. With the built-in
calendar, a January 2016 input raises ValueError because the previous month
(December 2015) is outside coverage.
>>> from pyworkdates import last_workday_of_previous_month
>>> last_workday_of_previous_month("2026-01-15") # crosses the year boundary
datetime.date(2025, 12, 31)
is_first_workday_of_month(date, calendar=None) -> bool
True if the date is its month's first workday. A date that is not itself a workday is never its month's first workday.
>>> from pyworkdates import is_first_workday_of_month
>>> is_first_workday_of_month("2026-05-04") # the Monday after May 1st
True
>>> is_first_workday_of_month("2026-05-01") # a holiday is not a workday
False
is_last_workday_of_month(date, calendar=None) -> bool
True if the date is its month's last workday. A date that is not itself a workday is never its month's last workday.
>>> from pyworkdates import is_last_workday_of_month
>>> is_last_workday_of_month("2026-05-29") # the last Friday of May 2026
True
>>> is_last_workday_of_month("2026-05-31") # a Sunday is not a workday
False
workdays_between(start, end, calendar=None) -> list[datetime.date]
All workdays d with start <= d <= end, in ascending order. Both endpoints
are included when they are workdays. If start is after end, the result is
an empty list.
>>> from pyworkdates import workdays_between
>>> workdays_between("2026-07-09", "2026-07-13") # Thursday through Monday
[datetime.date(2026, 7, 9), datetime.date(2026, 7, 10), datetime.date(2026, 7, 13)]
>>> workdays_between("2026-07-10", "2026-07-06") # start after end
[]
WorkdayCalendar(calendar=None)
A reusable calendar exposing every function above as a method. Define the
calendar once and call the operations without repeating the calendar
argument; each method behaves exactly like the module-level function of the
same name. Calendar entries are validated at construction, so invalid
entries are reported immediately; the methods then delegate to the
module-level functions with that calendar. WorkdayCalendar() uses the
built-in Norwegian calendar, including its coverage guard.
>>> from pyworkdates import WorkdayCalendar
>>> us = WorkdayCalendar(["2026-07-03", "2026-11-26"])
>>> us.is_workday("2026-07-03")
False
>>> us.add_workdays("2026-07-02", 1)
datetime.date(2026, 7, 6)
>>> WorkdayCalendar()
WorkdayCalendar(built-in Norwegian calendar, 2016-2050)
Development
The project uses uv with a src/ layout and no
runtime dependencies.
uv sync # install dev dependencies
uv run pytest # tests
uv run ruff check . # lint
uv run ruff format . # format
uv run ty check # type check
uv build # build sdist + wheel
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyworkdates-0.1.0.tar.gz.
File metadata
- Download URL: pyworkdates-0.1.0.tar.gz
- Upload date:
- Size: 11.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2de01251ffd97031fcae52768d884806de8a11a8abf44f9e6fee00b389988ac
|
|
| MD5 |
8c4acdb11a32fbdcf0b4dfb406974302
|
|
| BLAKE2b-256 |
f4885c4ba2adcb8051622250b7631b0cca0247de2b62caf783b379589897770c
|
File details
Details for the file pyworkdates-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyworkdates-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b3d45b9b283c7362690ea8ed0c90854346f95ec6420f4b36d2450ad2ba3b927
|
|
| MD5 |
61b37646ff26ab1c7bde123cc85afcaa
|
|
| BLAKE2b-256 |
ca381f3773c474bf17268c6e4fc02ea7f2f7188a63abf39aaff6d8a1eb49323a
|