Skip to content
Draft
25 changes: 24 additions & 1 deletion docs/holiday_categories.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,29 @@ for date, name in sorted(germany_catholic.items()):
print(f"{date}: {name}")
```

### School Holidays Example (Belgium)

Belgium provides school holidays that depend on the educational community.
Currently supported subdivisions are:

- `VLG` → Flemish Community
- `WBR` → French Community (Wallonia + Brussels French schools)
- `GER` → German-speaking Community

To retrieve only school holidays, use the `SCHOOL` category.

#### Basic Example

```python
import holidays
from holidays.constants import SCHOOL

# Flemish Community school holidays
be_flemish_schools = holidays.Belgium(subdiv="VLG", categories=SCHOOL, years=2026)
for date, name in sorted(be_flemish_schools.items()):
print(date, name)
```

### Unofficial Holidays Example

Get unofficial holidays in the United States:
Expand All @@ -307,7 +330,7 @@ Get unofficial holidays in the United States:
import holidays
from holidays.constants import UNOFFICIAL

us_unofficial = holidays.UnitedStates(categories=UNOFFICIAL, years=2024)
us_unofficial = holidays.UnitedStates(categories=UNOFFICIAL, years=2022)
for date, name in sorted(us_unofficial.items()):
print(f"{date}: {name}")
```
Expand Down
257 changes: 254 additions & 3 deletions holidays/countries/belgium.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,63 @@
# Website: https://github.com/vacanza/holidays
# License: MIT (see LICENSE file)

from datetime import date
from gettext import gettext as tr

from holidays.constants import BANK, PUBLIC
from holidays.calendars.gregorian import (
APR,
AUG,
JAN,
JUL,
JUN,
SEP,
OCT,
NOV,
DEC,
MON,
SAT,
_timedelta,
_get_nth_weekday_from,
_get_nth_weekday_of_month,
)
from holidays.constants import BANK, PUBLIC, SCHOOL
from holidays.groups import ChristianHolidays, InternationalHolidays
from holidays.holiday_base import HolidayBase


class Belgium(HolidayBase, ChristianHolidays, InternationalHolidays):
"""Belgium holidays.
"""
Belgium holidays.

References:
* <https://en.wikipedia.org/wiki/Public_holidays_in_Belgium>
* <https://web.archive.org/web/20250331001402/https://www.belgium.be/nl/over_belgie/land/belgie_in_een_notendop/feestdagen>
* <https://nl.wikipedia.org/wiki/Feestdagen_in_België>
* <https://web.archive.org/web/20240816004739/https://www.nbb.be/en/about-national-bank/national-bank-belgium/public-holidays>
* Official education calendars:
- [Flemish Community (Vlaanderen)](https://www.vlaanderen.be/onderwijs-en-vorming/wat-mag-en-moet-op-school/schoolvakanties-vrije-dagen-en-afwezigheden/schoolvakanties)
- [French Community (Fédération Wallonie-Bruxelles)](http://www.enseignement.be/index.php?page=23953)
- [German-speaking Community (Deutschsprachige Gemeinschaft)](https://ostbelgienbildung.be/desktopdefault.aspx/tabid-2212/4397_read-31727/)

Notes:
* Belgium has three school systems with different vacation rules:
- VLG: Flemish Community
- WBR: French Community (Wallonia + Brussels French schools)
- GER: German-speaking Community (Ostbelgien)
* School holiday rules are based on official education calendars.
"""

country = "BE"
default_language = "nl"
supported_categories = (BANK, PUBLIC)
supported_categories = (BANK, PUBLIC, SCHOOL)
supported_languages = ("de", "en_US", "fr", "nl", "uk")

subdivisions = (
"VLG", # Flemish Community.
"WBR", # French Community (Wallonia + Brussels French schools)
"GER", # German-speaking Community.
)

def __init__(self, *args, **kwargs):
ChristianHolidays.__init__(self)
InternationalHolidays.__init__(self)
Expand All @@ -48,10 +83,10 @@
self._add_easter_monday(tr("Paasmaandag"))

# Labor Day.
self._add_labor_day(tr("Dag van de Arbeid"))

Check failure on line 86 in holidays/countries/belgium.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Dag van de Arbeid" 4 times.

See more on https://sonarcloud.io/project/issues?id=vacanza_python-holidays&issues=AZzKaRIHBZFy4ifrFW_y&open=AZzKaRIHBZFy4ifrFW_y&pullRequest=3195

# Ascension Day.
self._add_ascension_thursday(tr("O. L. H. Hemelvaart"))

Check failure on line 89 in holidays/countries/belgium.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "O. L. H. Hemelvaart" 4 times.

See more on https://sonarcloud.io/project/issues?id=vacanza_python-holidays&issues=AZzYLNYHmYODFyIIc0xx&open=AZzYLNYHmYODFyIIc0xx&pullRequest=3195

# Whit Sunday.
self._add_whit_sunday(tr("Pinksteren"))
Expand Down Expand Up @@ -84,6 +119,222 @@
# Bank Holiday.
self._add_christmas_day_two(tr("Banksluitingsdag"))

def _add_multiday_holiday(
self, start_date: date, duration_days: int, *, name: str | None = None
) -> set[date]:
"""Override to start adding holidays directly from start_date."""
return super()._add_multiday_holiday(
_timedelta(start_date, -1),
duration_days=duration_days,
name=name,
)

def _populate_subdiv_vlg_school_holidays(self):
"""
School holidays for the Flemish Community (Vlaamse Gemeenschap).

Most vacation periods are rule-based and can be calculated
algorithmically, therefore they are implemented here.
"""
year = self._year
easter = self._easter_sunday

Comment thread
KJhellico marked this conversation as resolved.
# Christmas holidays.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Christmas holidays.
# Christmas Holidays.

Pay closer attention to details, it's important.
And I find it useful to leave a short description of the rules for each vacation period before the corresponding block, as was done in your previous editions.

name = tr("Kerstvakantie")
christmas = self._christmas_day
christmas_start = _get_nth_weekday_from(
1 if self._is_weekend(christmas) else -1, MON, christmas
)

self._add_multiday_holiday(christmas_start, 32 - christmas_start.day, name=name)

# Previous year's Christmas (for January spillover).
prev_christmas = self._christmas_day.replace(year=self._year - 1)
prev_christmas_start = _get_nth_weekday_from(
1 if self._is_weekend(prev_christmas) else -1,
MON,
prev_christmas,
)
duration = 14 - (32 - prev_christmas_start.day)
self._add_multiday_holiday(date(self._year, JAN, 1), duration, name=name)
Comment on lines +152 to +159
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
prev_christmas = self._christmas_day.replace(year=self._year - 1)
prev_christmas_start = _get_nth_weekday_from(
1 if self._is_weekend(prev_christmas) else -1,
MON,
prev_christmas,
)
duration = 14 - (32 - prev_christmas_start.day)
self._add_multiday_holiday(date(self._year, JAN, 1), duration, name=name)
new_year = date(self._year, JAN, 1)
christmas_last_part_duration = (4 - new_year.weekday()) % 7 + 3
self._add_multiday_holiday(new_year, christmas_last_part_duration, name=name)

The logic is a little confusing, but it works. ;)


# Carnival holidays.
carnival_raw = _timedelta(easter, days=-47)
carnival_start = _get_nth_weekday_from(-1, MON, carnival_raw)
self._add_multiday_holiday(carnival_start, 7, name=tr("Krokusvakantie"))
Comment on lines +161 to +164
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Carnival holidays.
carnival_raw = _timedelta(easter, days=-47)
carnival_start = _get_nth_weekday_from(-1, MON, carnival_raw)
self._add_multiday_holiday(carnival_start, 7, name=tr("Krokusvakantie"))
# Carnival Holidays: Carnival Monday (Easter - 48 days) → 1 week.
# Carnival Holidays.
self._add_multiday_holiday(_timedelta(easter, -48), 7, name=tr("Krokusvakantie"))

You missed it.


# Easter holidays.
# - March Easter → week before Easter.
# - Easter after 15 April → second Monday before Easter.
# - Otherwise → first Monday of April.
easter_start = _get_nth_weekday_of_month(1, MON, APR, self._year)
easter_duration = 14

if easter.month == 3:
easter_start = _timedelta(easter, -6)
elif easter.day >= 16:
easter_start = _timedelta(easter, -13)
easter_duration = 15
self._add_multiday_holiday(easter_start, easter_duration, name=tr("Paasvakantie"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._add_multiday_holiday(easter_start, easter_duration, name=tr("Paasvakantie"))
# Easter Holidays.
self._add_multiday_holiday(easter_start, easter_duration, name=tr("Paasvakantie"))


# Labor Day.
self._add_labor_day(tr("Dag van de Arbeid"))

# Ascension Day.
self._add_ascension_thursday(tr("O. L. H. Hemelvaart"))

# Friday after Ascension.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Friday after Ascension.
# Friday after Ascension Day.

self._add_holiday_40_days_past_easter(tr("Vrijdag na O. L. H. Hemelvaart"))

# Whit Monday.
self._add_whit_monday(tr("Pinkstermaandag"))

# Summer holidays.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Summer holidays.
# Summer Holidays.

self._add_multiday_holiday(date(year, JUL, 1), 62, name=tr("Zomervakantie"))

# Autumn holidays.
nov_1 = date(year, NOV, 1)
autumn_start = _get_nth_weekday_from(1 if self._is_sunday(nov_1) else -1, MON, nov_1)
self._add_multiday_holiday(autumn_start, 7, name=tr("Herfstvakantie"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._add_multiday_holiday(autumn_start, 7, name=tr("Herfstvakantie"))
# Autumn Holidays.
self._add_multiday_holiday(autumn_start, 7, name=tr("Herfstvakantie"))

All of these are l10n comments.


# Armistice Day.
self._add_remembrance_day(tr("Wapenstilstand"))

def _populate_subdiv_wbr_school_holidays(self):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 2022, the school year schedule in the French-speaking community has changed, which must be considered.

"""
School holidays for the French Community (Wallonie-Bruxelles).
Based on the official compulsory education calendar.
"""
year = self._year
easter = self._easter_sunday

# Autumn holidays.
autumn_start = _get_nth_weekday_of_month(3, MON, OCT, year)
self._add_multiday_holiday(autumn_start, 14, name=tr("Herfstvakantie"))

# current year's Christmas.
christmas = self._christmas_day
name = tr("Kerstvakantie")
christmas_start = _get_nth_weekday_from(
1 if self._is_weekend(christmas) else -1, MON, christmas
)
self._add_multiday_holiday(christmas_start, 32 - christmas_start.day, name=name)

# Previous year's Christmas (for January spillover).
prev_christmas = self._christmas_day.replace(year=self._year - 1)
prev_christmas_start = _get_nth_weekday_from(
1 if self._is_weekend(prev_christmas) else -1,
MON,
prev_christmas,
)
duration = 14 - (32 - prev_christmas_start.day)
self._add_multiday_holiday(date(self._year, JAN, 1), duration, name=name)

# Carnival holidays.
carnival_raw = _timedelta(easter, days=-49)
carnival_start = _get_nth_weekday_from(1, MON, carnival_raw)
self._add_multiday_holiday(carnival_start, 14, name=tr("Krokusvakantie"))

# Spring Holidays.
spring_start = _get_nth_weekday_of_month(4, MON, APR, year)
self._add_multiday_holiday(spring_start, 14, name=tr("Paasvakantie"))
Comment on lines +238 to +240
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Give WBR spring break its own source string.

tr("Paasvakantie") is already translated as "Easter Holidays" in holidays/locale/en_US/LC_MESSAGES/BE.po, so this subdivision can never surface "Spring Holidays" in non-Dutch locales. Use a distinct Dutch msgid for the WBR term and add matching entries to the BE .po files.

Based on learnings, msgid fields in localization files contain strings in the entity’s default language and msgstr should be a faithful translation of that msgid.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holidays/countries/belgium.py` around lines 238 - 240, The current code calls
tr("Paasvakantie") in the Belgium holiday block (see _get_nth_weekday_of_month
and self._add_multiday_holiday use) but that msgid is shared with the general
Easter Holidays; create a distinct Dutch msgid for the WBR spring break (e.g.
replace tr("Paasvakantie") with tr("WBR Paasvakantie") in the
self._add_multiday_holiday call) and add corresponding entries to the BE .po
files (and other locale .po files as needed) so the WBR term can be translated
independently from the existing Easter Holidays translation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KJhellico
Since Paasvakantie is already used for Easter holidays in other subdivisions, should WBR use a separate msgid (e.g. Lentevakantie) for Spring Holidays, or keep Paasvakantie for consistency?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


# Summer holidays.
summer_start = _get_nth_weekday_of_month(1, SAT, JUL, year)
summer_end = _get_nth_weekday_of_month(4, MON, AUG, year)
duration = (summer_end - summer_start).days
self._add_multiday_holiday(summer_start, duration, name=tr("Zomervakantie"))

# Armistice Day.
self._add_remembrance_day(tr("Wapenstilstand"))

# Labour Day.
self._add_labor_day(tr("Dag van de Arbeid"))

# French Community Day.
self._add_holiday(tr("Feestdag van de Franse Gemeenschap"), date(year, SEP, 27))

# Easter Monday.
self._add_easter_monday(tr("Paasmaandag"))

# Ascension Day.
self._add_ascension_thursday(tr("O. L. H. Hemelvaart"))

# Whit Monday.
self._add_whit_monday(tr("Pinkstermaandag"))

def _populate_subdiv_ger_school_holidays(self):
"""
School holidays for the German-speaking Community (Deutschsprachige Gemeinschaft).

Based on official education calendar structure.
Most vacation periods follow the same framework as other Belgian communities,
with Easter holidays starting on the Monday after Easter.
"""
year = self._year
easter = self._easter_sunday

# Christmas holidays.
christmas = self._christmas_day
name = tr("Kerstvakantie")

# if Christmas is on Monday, holiday will start that day.
# if Christmas is on Tue-Fri, start the previous Monday.
# if Christmas is a Sat/Sun, start the following Monday.
christmas_start = _get_nth_weekday_from(
1 if self._is_weekend(christmas) else -1, MON, christmas
)
self._add_multiday_holiday(christmas_start, 32 - christmas_start.day, name=name)

# Previous year's Christmas (for January spillover).
prev_christmas = self._christmas_day.replace(year=self._year - 1)
prev_christmas_start = _get_nth_weekday_from(
1 if self._is_weekend(prev_christmas) else -1,
MON,
prev_christmas,
)
duration = 14 - (32 - prev_christmas_start.day)
self._add_multiday_holiday(date(self._year, JAN, 1), duration, name=name)

# Carnival holidays.
carnival_raw = _timedelta(easter, days=-47)
carnival_start = _get_nth_weekday_from(-1, MON, carnival_raw)
self._add_multiday_holiday(carnival_start, 6, name=tr("Krokusvakantie"))

# Easter holidays.
# Starts on the Monday after Easter.
easter_start = _timedelta(easter, 1)
self._add_multiday_holiday(easter_start, 13, name=tr("Paasvakantie"))

# Summer holidays.
self._add_multiday_holiday(date(self._year, JUN, 30), 63, name=tr("Zomervakantie"))

# Autumn holidays.
nov_1 = date(year, NOV, 1)
autumn_start = _get_nth_weekday_from(1 if self._is_sunday(nov_1) else -1, MON, nov_1)
self._add_multiday_holiday(autumn_start, 6, name=tr("Herfstvakantie"))

# Armistice Day.
self._add_remembrance_day(tr("Wapenstilstand"))

# Labour Day.
self._add_labor_day(tr("Dag van de Arbeid"))

# German Community Day.
self._add_holiday(tr("Feestdag van de Duitstalige Gemeenschap"), date(year, NOV, 15))

# Easter Monday.
self._add_easter_monday(tr("Paasmaandag"))

# Ascension Day.
self._add_ascension_thursday(tr("O. L. H. Hemelvaart"))

# Whit Monday.
self._add_whit_monday(tr("Pinkstermaandag"))

# Christmas Eve.
self._add_holiday(tr("Heiligavond"), date(year, DEC, 24))


class BE(Belgium):
pass
Expand Down
32 changes: 32 additions & 0 deletions holidays/locale/de/LC_MESSAGES/BE.po
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,35 @@ msgstr "Freitag nach Christi Himmelfahrt"
#. Bank Holiday.
msgid "Banksluitingsdag"
msgstr "Bankschlusstag"

#. Christmas Holidays.
msgid "Kerstvakantie"
msgstr "Weihnachtsferien"

#. Carnival Holidays.
msgid "Krokusvakantie"
msgstr "Karnevalsferien"

#. Easter Holidays.
msgid "Paasvakantie"
msgstr "Osterferien"

#. Summer Holidays.
msgid "Zomervakantie"
msgstr "Sommerferien"

#. Autumn Holidays.
msgid "Herfstvakantie"
msgstr "Herbstferien"

#. French Community Day.
msgid "Feestdag van de Franse Gemeenschap"
msgstr "Feiertag der Französischen Gemeinschaft"

#. German-speaking Community Day.
msgid "Feestdag van de Duitstalige Gemeenschap"
msgstr "Feiertag der Deutschsprachigen Gemeinschaft"

#. Christmas Eve.
msgid "Heiligavond"
msgstr "Heiligabend"
Loading