DEV Community

UpcomingDays
UpcomingDays

Posted on AI-assisted

Your holiday library is probably right. Here are the days it got wrong for us, and the correction layer we built

If you calculate business days, delivery dates or payroll in more than one country, you probably use the excellent open-source holidays package for Python. We do too. It covers more than 200 countries and territories and is right almost all of the time.

"Almost" matters when you publish holiday lists that people plan around. So before we list a country, we compare the library's dates with the government's own announcements. Thirteen countries needed at least one correction. None of these are really bugs. Governments publish decisions the library can't know in advance. But the patterns are worth knowing if holiday data feeds anything important.

The five kinds of mismatch we found

1. Holidays moved by decree to make a long weekend. Kuwait and Oman often move a holiday to a Thursday or Sunday. The library computes the "natural" date:

Country Holiday (2026) holidays 0.105 Official
Kuwait Prophet's Birthday Aug 25 (estimated) Aug 27
Oman Sultan's Accession Day Jan 11 Jan 15
Oman National Day Nov 20–21 Nov 25–26

2. Lunar dates confirmed differently from the estimate. Islamic holidays depend on a moon sighting or an official calendar. Tanzania announced Eid al-Adha 2026 for May 27, where the library has May 28. India's central government list puts Holi 2027 on March 23, a day after the library's date.

3. A fixed date that's just off by one. Morocco's Amazigh New Year is January 14 by royal decree. The library has January 13.

4. Missing days. Morocco added Unity Day (October 31) as a national holiday from 2026. Ethiopia's statutory list includes Downfall of the Derg (May 28). Costa Rica has three days that are paid only if worked: August 2, August 31 and December 1. Whether those count as "holidays" depends on your use case, which is exactly why you want to decide that explicitly.

5. Regional days listed as national. Guatemala's Assumption Day (August 15) is a holiday in Guatemala City only, not nationwide.

The correction layer

We didn't want to fork the library. Instead, a thin layer sits on top with three tables: date overrides, additions and removals. In our production version, each entry has a comment naming the official source.

import warnings
import holidays

# (country, name, date the library gives) -> official date
OVERRIDES = {
    ("MA", "Amazigh New Year", "2026-01-13"): "2026-01-14",
    ("KW", "Prophet's Birthday (estimated)", "2026-08-25"): "2026-08-27",
}
# Nationwide days the library is missing: (country, year) -> [(date, name)]
ADDITIONS = {
    ("MA", 2026): [("2026-10-31", "Unity Day")],
    ("CR", 2026): [("2026-08-02", "Our Lady of the Angels Day")],
}
# Listed as nationwide, but regional only: (country, name)
REMOVALS = {("GT", "Assumption Day")}

def corrected(code, year):
    used = set()
    rows = []
    for d, name in sorted(holidays.country_holidays(code, years=year, language="en_US").items()):
        if (code, name) in REMOVALS:
            continue
        key = (code, name, d.isoformat())
        if key in OVERRIDES:
            used.add(key)
            rows.append((OVERRIDES[key], name.replace(" (estimated)", "")))
        else:
            rows.append((d.isoformat(), name))
    rows += ADDITIONS.get((code, year), [])
    # An override that matched nothing means the library changed: re-check it.
    for key in OVERRIDES:
        if key[0] == code and key[2].startswith(str(year)) and key not in used:
            warnings.warn(f"stale override {key}")
    return sorted(rows)

for code in ("MA", "KW", "GT"):
    for row in corrected(code, 2026):
        if any(k in row[1] for k in ("Amazigh", "Unity", "Prophet", "Assumption")):
            print(code, *row)
Enter fullscreen mode Exit fullscreen mode

With holidays==0.105:

MA 2026-01-14 Amazigh New Year
MA 2026-08-25 Prophet's Birthday (estimated)
MA 2026-08-26 Prophet's Birthday (estimated)
MA 2026-10-31 Unity Day
KW 2026-08-27 Prophet's Birthday
Enter fullscreen mode Exit fullscreen mode

Guatemala's Assumption Day is gone, Kuwait's date is moved, and Morocco gains Unity Day.

Three details made this hold up over time:

  • Key overrides on the library's own value, not just the holiday name. ("MA", "Amazigh New Year", "2026-01-13") only applies while the library still says January 13. If a new release fixes it upstream, the key stops matching.
  • Warn on stale overrides. That unmatched key then raises a warning instead of silently doing nothing. Our first run of this snippet showed why: without language="en_US", Morocco's names come back in Arabic, nothing matched, and the warnings fired straight away.
  • Clear the "estimated" flag when you apply an official date. The library marks some lunar dates as (estimated). Once you've replaced one with a government-announced date, it isn't an estimate any more, and your UI shouldn't say it is.

Where the official dates come from

Each correction has a primary source: a government circular (Kuwait, Bahrain, Jordan), a labour ministry calendar (Oman, Costa Rica), a royal decree (Morocco), India's DoPT holiday order for central government offices, or national news coverage of the announcement (Tanzania). Record the source next to the override, because you'll need it again next year when the dates change.

Please report what you find upstream as well. A fix in the library is better than a fix in everyone's correction layer.

We use this layer for the country holiday pages on UpcomingDays, where I work. The snippet above is standalone and needs only pip install holidays.

This article was written with the help of AI. The code was run against holidays 0.105, and each correction was checked against the official source named in our data before publishing.

Top comments (1)

Collapse
 
arhancanli profile image
Arhan Canli •

Keying the override on the library's own value is a good design. The stale-override warning only covers OVERRIDES, though, and the other two tables fail quietly when upstream changes.

  • ADDITIONS: if holidays later adds Morocco's Unity Day for 2026, rows += ADDITIONS.get(...) appends a second copy and nothing warns. A set of (date, name) or a check that the date is not already in rows would catch it.
  • REMOVALS: it matches on the exact English name. If a release renames "Assumption Day" for Guatemala, or the library starts marking it regional itself, the entry silently does nothing, which is the same failure you guarded against with the Arabic-names run. The same used set can warn on a removal that matched nothing.

One more thing in your own output: Morocco's Prophet's Birthday still prints "(estimated)" for Aug 25 and 26, while Kuwait's loses the flag only because it has an override. If the UI shows that label, it is worth deciding whether the unconfirmed lunar dates should be shown as ranges. I read the code rather than ran it.