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)
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
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)
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.
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.usedset 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.