How to Calculate Business Days in Python

The Challenge#

Calculating business days sounds straightforward until you consider:

  • Holidays vary by country — New Year's Day is January 1st in most places, but not everywhere

  • Workweeks differ globally — Israel uses Sunday-Thursday, UAE uses Monday-Friday (recently changed from Sunday-Thursday)

  • Regional holidays exist — US states have different holidays; German Länder celebrate different days

  • Some holidays move — Easter, Thanksgiving, and lunar calendar holidays change yearly

  • Half-day holidays — Christmas Eve is a half-day in some countries, full day in others

Python's built-in date libraries don't handle any of this. This guide shows you how to solve it.

Prerequisites#

Before starting, you need:

  • Python 3.8+ installed

  • pip for installing packages

  • A World Data API key (for the API-based approach) — get a free key

Install the required packages:

bash
pip install requests numpy

For the workalendar approach, also install:

bash
pip install workalendar

Note: The World Data API business days endpoint requires a paid tier (Starter or higher). The free tier provides access to basic country and holiday data.

Quick Solution#

python
import requests

# Add 10 business days to a date, excluding US holidays
response = requests.get(
    "https://worlddataapi.com/v1/business-days/US/add",
    params={"start": "2026-01-15", "days": 10},
    headers={"X-API-Key": "YOUR_API_KEY"}
)
result = response.json()
print(result["result"])  # "2026-01-29"

Or using curl:

bash
curl -X GET "https://worlddataapi.com/v1/business-days/US/add?start=2026-01-15&days=10" \
  -H "X-API-Key: YOUR_API_KEY"

That's the complete solution for holiday-aware business day calculations. The rest of this guide covers Python's built-in options, their limitations, and when to use each approach.

Python's Built-in Options#

NumPy's busday_count#

NumPy provides fast business day calculations:

python
import numpy as np

# Count business days between two dates
start = np.datetime64('2026-01-01')
end = np.datetime64('2026-01-31')
business_days = np.busday_count(start, end)
print(business_days)  # 22

The problem: NumPy doesn't know about holidays.

python
# January 1, 2026 is New Year's Day — not a business day
# But NumPy counts it as one
np.busday_count('2025-12-31', '2026-01-02')  # Returns 1, should be 0

You can pass holidays manually:

python
holidays = ['2026-01-01', '2026-01-19']  # New Year's, MLK Day
np.busday_count('2026-01-01', '2026-01-31', holidays=holidays)  # 20

But now you're maintaining holiday data yourself.

Adding Business Days with NumPy#

python
import numpy as np

# Add 10 business days
start = np.datetime64('2026-01-15')
result = np.busday_offset(start, 10)
print(result)  # 2026-01-29

Same limitation — no holiday awareness without manual data.

The datetime and dateutil Approach#

For basic weekday skipping without NumPy:

python
from datetime import datetime, timedelta

def add_business_days(start_date: datetime, days: int) -> datetime:
    """Add business days, skipping weekends only."""
    result = start_date
    added = 0

    while added < days:
        result += timedelta(days=1)
        if result.weekday() < 5:  # Monday = 0, Friday = 4
            added += 1

    return result

# Add 10 business days
start = datetime(2026, 1, 15)
result = add_business_days(start, 10)
print(result.date())  # 2026-01-29

This fails on holidays and non-Western workweeks, just like the NumPy version.

The workalendar Library#

workalendar provides holiday calendars for many countries:

python
from workalendar.usa import UnitedStates
from datetime import date

cal = UnitedStates()

# Check if a date is a business day
print(cal.is_working_day(date(2026, 1, 1)))  # False (New Year's)
print(cal.is_working_day(date(2026, 1, 2)))  # True

# Add business days
result = cal.add_working_days(date(2026, 1, 15), 10)
print(result)  # 2026-01-30 (accounts for MLK Day)

workalendar covers 70+ countries with state/province support for some:

python
from workalendar.usa import California

cal = California()
# Includes César Chávez Day (March 31)

workalendar Limitations#

  • Coverage gaps for some countries and regions

  • Doesn't handle all regional holidays

  • Updates lag behind government announcements

  • Islamic holidays use calculated dates (not moon-sighted)

For applications where accuracy matters, verify workalendar's data against official sources.

API-Based Solution#

World Data API handles business day calculations directly, with current holiday data for 230+ countries:

python
import requests
from typing import Optional

class BusinessDays:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://worlddataapi.com/v1/business-days"
        self.session = requests.Session()
        self.session.headers["X-API-Key"] = api_key

    def is_business_day(self, location: str, date: str) -> dict:
        """Check if a date is a business day."""
        response = self.session.get(
            f"{self.base_url}/{location}",
            params={"date": date}
        )
        response.raise_for_status()
        return response.json()

    def add(self, location: str, start: str, days: int,
            day_type: str = "business") -> dict:
        """Add business days to a date."""
        response = self.session.get(
            f"{self.base_url}/{location}/add",
            params={"start": start, "days": days, "type": day_type}
        )
        response.raise_for_status()
        return response.json()

    def subtract(self, location: str, start: str, days: int,
                 day_type: str = "business") -> dict:
        """Subtract business days from a date."""
        response = self.session.get(
            f"{self.base_url}/{location}/subtract",
            params={"start": start, "days": days, "type": day_type}
        )
        response.raise_for_status()
        return response.json()

    def count(self, location: str, start: str, end: str,
              day_type: str = "business") -> dict:
        """Count business days between two dates."""
        response = self.session.get(
            f"{self.base_url}/{location}/count",
            params={"start": start, "end": end, "type": day_type}
        )
        response.raise_for_status()
        return response.json()

Usage Examples#

python
import os

api = BusinessDays(os.environ["WORLD_DATA_API_KEY"])

# Check if New Year's Day is a business day
result = api.is_business_day("US", "2026-01-01")
print(result["is_business_day"])  # False
print(result["reason"])  # "holiday"
print(result["holiday"]["name"])  # "New Year's Day"

# Add 10 business days
result = api.add("US", "2026-01-15", 10)
print(result["result"])  # "2026-01-29"

# Count business days in January (California)
result = api.count("US-CA", "2026-01-01", "2026-01-31")
print(result["business_days"])  # 21
print(result["holidays"])  # 2
for holiday in result["holiday_list"]:
    print(f"  {holiday['date']}: {holiday['name']}")

Handling Different Workweeks#

Israel uses Sunday-Thursday. The API returns the workweek in responses:

python
result = api.is_business_day("IL", "2026-01-02")  # Friday
print(result["is_business_day"])  # False
print(result["reason"])  # "weekend"
print(result["workweek"])  # ["sunday", "monday", "tuesday", "wednesday", "thursday"]

Banking vs Business Days#

Financial applications often need banking days:

python
# UK August bank holiday affects banking days
result = api.add("GB", "2026-08-25", 5, day_type="banking")
print(result["result"])  # Accounts for bank holiday

Day types:

  • business — weekends + public holidays

  • banking — weekends + public holidays + bank holidays

  • government — weekends + public holidays + government closures

Combining Approaches#

For high-volume applications, fetch holidays once and calculate locally:

python
import os
import requests
from datetime import datetime, timedelta
from functools import lru_cache

@lru_cache(maxsize=100)
def get_holidays(location: str, year: int) -> frozenset:
    """Fetch and cache holidays for a location and year."""
    response = requests.get(
        f"https://worlddataapi.com/v1/holidays/{location}",
        params={"year": year},
        headers={"X-API-Key": os.environ['WORLD_DATA_API_KEY']}
    )
    response.raise_for_status()
    data = response.json()
    return frozenset(h["date"] for h in data["holidays"])

def add_business_days_cached(location: str, start: datetime, days: int) -> datetime:
    """Add business days using cached holiday data."""
    holidays = get_holidays(location, start.year)
    result = start
    added = 0

    while added < days:
        result += timedelta(days=1)

        # Check if we crossed into a new year
        if result.year != start.year:
            holidays = holidays | get_holidays(location, result.year)

        date_str = result.strftime("%Y-%m-%d")
        if result.weekday() < 5 and date_str not in holidays:
            added += 1

    return result

This approach:

  • Reduces API calls for repeated calculations

  • Works offline after initial fetch

  • Trades accuracy for speed (cached data may become stale)

Error Handling#

python
import os
import requests
from requests.exceptions import RequestException

def safe_add_business_days(location: str, start: str, days: int) -> dict:
    try:
        response = requests.get(
            f"https://worlddataapi.com/v1/business-days/{location}/add",
            params={"start": start, "days": days},
            headers={"X-API-Key": os.environ['WORLD_DATA_API_KEY']},
            timeout=10
        )

        if response.status_code == 400:
            raise ValueError("Invalid date format or location code")
        if response.status_code == 401:
            raise PermissionError("Invalid API key")
        if response.status_code == 429:
            raise RuntimeError("Rate limit exceeded — retry later")

        response.raise_for_status()
        return response.json()

    except requests.exceptions.Timeout:
        raise TimeoutError("API request timed out")
    except requests.exceptions.ConnectionError:
        raise ConnectionError("Network error — check your connection")

Comparison: When to Use What#

ApproachProsCons
NumPy busday_countFast, no dependencies beyond NumPyNo holidays, Mon-Fri only
datetime + timedeltaNo dependenciesNo holidays, Mon-Fri only
workalendarGood country coverage, offlineCoverage gaps, may lag updates
World Data API249 countries, current data, regional supportRequires network, API costs

Use NumPy/datetime when:#

  • You only need weekend skipping

  • Holidays aren't relevant (rough estimates)

  • Offline operation is required

Use workalendar when:#

  • You need holidays for supported countries

  • Offline operation is required

  • You can verify accuracy for your use case

Use an API when:#

  • Dates have legal/financial significance

  • You support multiple countries or regions

  • You need regional holidays (US states, German Länder)

  • Accuracy matters more than latency

Pandas Integration#

For dataframe operations:

python
import pandas as pd
import numpy as np

# Create a date range and identify business days
dates = pd.date_range('2026-01-01', '2026-01-31')

# Basic business day check (no holidays)
df = pd.DataFrame({'date': dates})
df['is_weekday'] = df['date'].dt.dayofweek < 5

# With holidays from API
holidays = get_holidays('US', 2026)
df['is_business_day'] = (
    (df['date'].dt.dayofweek < 5) &
    (~df['date'].dt.strftime('%Y-%m-%d').isin(holidays))
)

print(df[~df['is_business_day']])

Common Pitfalls#

Assuming Monday-Friday everywhere#

Israel, UAE, and other countries use different workweeks. Hardcoding weekday() < 5 breaks for these regions:

python
# This fails for Israel (Sunday-Thursday workweek)
if date.weekday() < 5:  # Wrong assumption
    business_days += 1

Forgetting to handle year boundaries#

When adding business days across December/January, you need holidays for both years:

python
# Bug: only fetches holidays for start year
holidays = get_holidays("US", start.year)

# Fix: fetch holidays for the result year too
if result.year != start.year:
    holidays = holidays | get_holidays("US", result.year)

Caching holidays without invalidation#

Holiday data can change (governments announce new holidays). Cache with a reasonable TTL:

python
# Set cache TTL to 24 hours, not forever
@lru_cache(maxsize=100)  # No TTL - stale data risk

# Better: use a time-aware cache or refresh periodically

Ignoring regional holidays#

US federal holidays differ from state holidays. California observes Cesar Chavez Day; other states don't:

python
# US federal holidays only
result = api.count("US", "2026-03-01", "2026-04-01")

# California-specific (includes Cesar Chavez Day)
result = api.count("US-CA", "2026-03-01", "2026-04-01")

Treating half-days as full days#

Some countries treat Christmas Eve as a half-day. The API treats it as a full holiday. For precise calculations, check your specific country's rules.

Limitations#

World Data API calculates Islamic holidays astronomically. Actual observance in some countries depends on physical moon sighting and may differ by 1-2 days.

Half-day holidays (Christmas Eve in some countries) are treated as full holidays.

Summary#

Python's built-in date functions and NumPy handle basic weekday calculations but ignore holidays entirely. The workalendar library adds holiday support for many countries but has coverage gaps and maintenance lag. For applications where accuracy matters — financial calculations, delivery estimates, legal deadlines — use an API with current holiday data.

Key takeaways:

  • NumPy busday_count works for rough estimates when holidays don't matter

  • workalendar provides offline holiday support for 70+ countries

  • World Data API covers 230+ countries with regional holidays and different workweek support

  • Always handle year boundaries when calculations span December/January

  • Remember that workweeks vary globally — not everyone works Monday-Friday

Ready to build accurate business day calculations? Get your API key and start with the free tier, or check out the API documentation for full endpoint details.

Next Steps#