Skip to main content

busdays

Zero-dependency business-day arithmetic with a configurable weekend mask and optional holidays: offsets, counts, roll conventions and month helpers.

Install

pip install busdays

Usage

from datetime import date

from busdays import (
    Calendar,
    Roll,
    add_business_days,
    business_days_between,
    is_business_day,
    next_business_day,
    prev_business_day,
)

friday = date(2026, 1, 2)

add_business_days(friday, 1)  # -> date(2026, 1, 5)
add_business_days(friday, -1)  # -> date(2026, 1, 1)
next_business_day(friday)  # -> date(2026, 1, 5)
prev_business_day(date(2026, 1, 5))  # -> date(2026, 1, 2)
is_business_day(date(2026, 1, 3))  # -> False

# You bring the holidays. Here a Monday holiday is skipped.
add_business_days(friday, 1, holidays={date(2026, 1, 5)})  # -> date(2026, 1, 6)

# Count business days in the half-open interval [a, b).
business_days_between(date(2026, 1, 5), date(2026, 1, 12))  # -> 5
business_days_between(date(2026, 1, 12), date(2026, 1, 5))  # -> -5

# Weekdays are integers, Monday is 0. A Friday and Saturday weekend:
add_business_days(date(2026, 1, 1), 1, weekend=(4, 5))  # -> date(2026, 1, 4)

A calendar, built once

A Calendar checks and sorts the weekend and holidays once; after that, counts and offsets take logarithmic time, so a million business days cost the same as one. The module-level functions build a calendar on every call, which costs O(H log H) for H holidays: build a Calendar yourself when you ask many questions of the same holidays. business_days() yields one date at a time, so it costs time in the number of days it covers. Holidays on a weekend day are dropped and duplicates merged, as numpy.busdaycalendar does.

holidays = [date(2026, 12, 25), date(2026, 12, 26), date(2026, 12, 28)]
uk = Calendar(holidays=holidays)
uk.holidays  # -> (date(2026, 12, 25), date(2026, 12, 28))
uk.add_business_days(date(2026, 12, 24), 2)  # -> date(2026, 12, 30)
uk.last_business_day(2026, 12)  # -> date(2026, 12, 31)
uk.first_business_day(2026, 2)  # -> date(2026, 2, 2)
days = uk.business_days(date(2026, 12, 24), date(2026, 12, 31))
[d.day for d in days]  # -> [24, 29, 30]
Calendar().add_business_days(date(2026, 1, 5), 1_000_000)  # -> date(5859, 1, 31)

# numpy style weekmasks: seven 0/1 characters, day names (any case) or seven flags.
gulf = Calendar.from_weekmask("Sun Mon Tue Wed Thu")
gulf.weekend  # -> (4, 5)
gulf.weekmask  # -> '1111001'

Roll conventions

roll moves a date that is not a business day; a business day is never moved. The conventions are the ISDA ones (following, modified following, preceding) and the common non-ISDA ones (modified preceding, half-month modified following, nearest, unadjusted), plus raise. Names are forgiving: "modified_following", "ModifiedFollowing" and numpy's "forward" and "backward" all work. numpy's "nat" has no date equivalent and is refused.

saturday = date(2026, 1, 31)
uk.roll(saturday)  # -> date(2026, 2, 2)
uk.roll(saturday, Roll.MODIFIED_FOLLOWING)  # -> date(2026, 1, 30)
uk.roll(saturday, "preceding")  # -> date(2026, 1, 30)
uk.roll(date(2026, 2, 14), Roll.HALF_MONTH_MODIFIED_FOLLOWING)  # -> date(2026, 2, 13)
uk.roll(date(2026, 1, 4), Roll.NEAREST)  # -> date(2026, 1, 5)
Roll("forward")  # -> Roll.FOLLOWING

# With roll=, the start is adjusted first and n is counted from it, as numpy does.
uk.add_business_days(saturday, 0, roll="following")  # -> date(2026, 2, 2)
uk.add_business_days(saturday, 1, roll="modifiedfollowing")  # -> date(2026, 2, 2)

Conventions and errors

  • Weekday integers follow date.weekday(): Monday is 0, Sunday is 6, and the default weekend is (5, 6).
  • Without roll, add_business_days counts only the days it lands on: the start need not be a business day, 0 returns the start unchanged, and any other count lands on a business day. That is numpy's backward roll for forward steps and forward roll for backward steps.
  • business_days_between(a, b) counts [a, b) and is antisymmetric: swapping the dates negates the count. numpy counts a reversed range as (b, a] instead, so the two differ when a or b is not a business day.
  • Dates must be datetime.date; a datetime is refused (TypeError) because it never equals a date holiday. Counts and weekdays must be integers, not bool or float.
  • A result before year 1 or after year 9999 raises OverflowError. A weekend covering all seven days raises ValueError whenever a business day has to be found.

Command line

busdays add 2026-01-02 1                      # 2026-01-05
busdays add 2026-01-31 0 --roll modifiedfollowing
busdays count 2026-01-05 2026-01-12           # 5
busdays roll 2026-01-31 preceding             # 2026-01-30
busdays is 2026-01-03                         # prints "no", exits 1
busdays list 2026-12-01 2026-12-31 --holidays uk.txt
busdays add 2026-01-01 1 --weekmask "Sun Mon Tue Wed Thu"

Every command takes --weekend 5,6 or --weekmask 1111100, --holiday DATE (repeat it) and --holidays FILE (one ISO date per line, # comments allowed, - for standard input). Bad input exits with status 2. python -m busdays works too.

Why

Business-day math keeps getting reinvented because the existing options pull their weight the wrong way. numpy.busday_offset and busday_count require numpy for a few lines of date math. workalendar and python-holidays are large and opinionated: they ship their own regional calendars, which is the opposite of what you want when the holidays come from your own database. This library is the small, pure primitive in between: you pass the weekend mask and the holiday set, it does the arithmetic, and it depends on nothing but the standard library. Its results are checked against numpy in the test suite.

License

MIT. See LICENSE.

Metadata

Release files for busdays 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for busdays 0.2.0
File Size Uploaded Provenance Actions
busdays-0.2.0.tar.gz 29.5 kB Without provenance Details for busdays-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for busdays 0.2.0
File Interpreter ABI Platform Actions
busdays-0.2.0-py3-none-any.whl Python 3 none any Details for busdays-0.2.0-py3-none-any.whl

Total release size: 44.8 kB

Release files / busdays-0.2.0.tar.gz

Download URL busdays-0.2.0.tar.gz
Size 29.5 kB
Tags Source
SHA-256 checksum
How to use checksums
6f1aaea49ede64d218e9a9fd418a5deb1b079b57eec977149964b3f13af43a65
BLAKE2b-256 checksum
How to use checksums
84e39059dfcc4fdc9a2ce87131eba12353f7ebf5cc81a1a4985f6154166e6d8a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / busdays-0.2.0-py3-none-any.whl

Download URL busdays-0.2.0-py3-none-any.whl
Size 15.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f72e9aa8d46ea8d633c821e96eba1919faee6f8c7ae63ab6de7b0fb25ad05d2c
BLAKE2b-256 checksum
How to use checksums
0c7e062f850782e9d9efb2f120dee058013651a8b14c1aff497b5c29650753f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Security for busdays 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page