Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()):
Comment on lines +319 to +321

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Example label and subdivision are inconsistent.

Line 319 says Flemish, but Line 320 uses subdiv="WBR" (French Community). Make both refer to the same community.

Suggested doc fix
-# Flemish Community school holidays
-be_flemish_schools = holidays.Belgium(subdiv="WBR", categories=SCHOOL, years=2025)
+# Flemish Community school holidays
+be_flemish_schools = holidays.Belgium(subdiv="VLG", categories=SCHOOL, years=2025)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Flemish Community school holidays
be_flemish_schools = holidays.Belgium(subdiv="WBR", categories=SCHOOL, years=2025)
for date, name in sorted(be_flemish_schools.items()):
# Flemish Community school holidays
be_flemish_schools = holidays.Belgium(subdiv="VLG", categories=SCHOOL, years=2025)
for date, name in sorted(be_flemish_schools.items()):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/holiday_categories.md` around lines 319 - 321, The example label and
subdivision mismatch: the variable be_flemish_schools and heading say "Flemish
Community" but the code uses subdiv="WBR" (Walloon/Brussels); update the subdiv
to the Flemish code (e.g., use subdiv="VLG") or change the heading/variable to
match WBR so both are consistent; adjust the instantiation in
holidays.Belgium(...) where be_flemish_schools and SCHOOL are used to reflect
the chosen community.

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
262 changes: 259 additions & 3 deletions holidays/countries/belgium.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,65 @@
# 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,
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,7 +85,7 @@
self._add_easter_monday(tr("Paasmaandag"))

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

Check failure on line 88 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"))
Expand Down Expand Up @@ -84,6 +121,225 @@
# 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,
)
Comment thread
KJhellico marked this conversation as resolved.

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.
"""
Comment on lines +130 to +135

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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider deduplicating the shared Christmas-break logic across VLG/WBR/GER.

The Christmas-break block (the December portion + January spillover with (4 - new_year.weekday()) % 7 + 3 duration) is copy-pasted verbatim into all three subdivision methods. Extracting a small private helper (e.g., _add_christmas_school_break(self)) would keep the three methods focused on what differs and avoid future drift.

Also applies to: 218-221, 307-313

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@holidays/countries/belgium.py` around lines 130 - 135, Extract the duplicated
Christmas-break calculation (the December period plus the January spillover
using the `(4 - new_year.weekday()) % 7 + 3` duration) into a single private
helper method (suggested name `_add_christmas_school_break(self, year,
holidays)` or similar) and replace the copy-pasted blocks in the three
subdivision methods for VLG/WBR/GER with calls to that helper; ensure the helper
accepts the year and the holidays collection (or uses the instance state) and
adds the same start/end dates so behavior remains identical.

year = self._year
easter = self._easter_sunday

Comment thread
KJhellico marked this conversation as resolved.
# Christmas holidays (week of 25 December).
christmas = date(year, DEC, 25)
start = _get_nth_weekday_from(-1 if christmas.weekday() <= 4 else 1, 0, christmas)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
self._add_multiday_holiday(start, 13, name=tr("Kerstvakantie"))
Comment thread
KJhellico marked this conversation as resolved.
Outdated
Comment thread
KJhellico marked this conversation as resolved.
Outdated

# Previous year's Christmas (for January spillover).
prev_christmas = date(year - 1, DEC, 25)
prev_start = _get_nth_weekday_from(
-1 if prev_christmas.weekday() <= 4 else 1,
0,
prev_christmas,
)
self._add_multiday_holiday(prev_start, 14, name=tr("Kerstvakantie"))

# Carnival holidays.
krokus_raw = _timedelta(easter, days=-47)
krokus_start = _get_nth_weekday_from(-1, MON, krokus_raw)
self._add_multiday_holiday(krokus_start, 7, name=tr("Krokusvakantie"))
Comment thread
KJhellico marked this conversation as resolved.
Outdated

# 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)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
elif easter.day >= 16:
easter_start = _timedelta(easter, -13)
easter_duration = 15
self._add_multiday_holiday(easter_start, easter_duration, name=tr("Paasvakantie"))
Comment thread
KJhellico marked this conversation as resolved.

# Labour Day.
Comment thread
KJhellico marked this conversation as resolved.
Outdated
self._add_labor_day(tr("Dag van de Arbeid"))

# Ascension Day.
self._add_ascension_thursday(tr("Hemelvaart"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Friday after Ascension.
Comment thread
KJhellico marked this conversation as resolved.
Outdated
ascension_friday = _timedelta(easter, days=40)
self._add_holiday(tr("Vrijdag na O. L. H. Hemelvaart"), ascension_friday)
Comment thread
KJhellico marked this conversation as resolved.
Outdated

# Whit Monday / Pentecost Monday.
Comment thread
KJhellico marked this conversation as resolved.
Outdated
self._add_whit_monday(tr("Pinkstermaandag"))
Comment on lines +185 to +195

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Public holidays are being re-emitted inside SCHOOL population methods.

Labor Day, Ascension Day, Friday after Ascension, Whit Monday, Armistice Day, Easter Monday, etc. are public holidays already populated via _populate_public_holidays. Duplicating them inside the SCHOOL subdivision methods creates several problems:

  • A user requesting categories=SCHOOL only gets PUBLIC dates leaking into SCHOOL output.
  • A user requesting categories=(PUBLIC, SCHOOL) gets duplicate-name conflicts on the same dates.
  • The set of "public" days varies inconsistently between VLG/WBR/GER methods (e.g., GER adds Christmas Eve and a "German Community Day"; VLG omits Easter Monday but includes Whit Monday; WBR adds French Community Day on Sep 27).

If the intent is "school-closed days that aren't already public holidays", drop the public-holiday calls and only register break periods + community-specific days (e.g., Sep 27 for WBR, Nov 15 for GER, Dec 24 for GER if school-specific). Otherwise the SCHOOL category will look like a mismatched superset of PUBLIC.

Also applies to: 274-290, 370-389

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@holidays/countries/belgium.py` around lines 185 - 195, The SCHOOL-population
methods are re-adding public holidays (calls to _add_labor_day,
_add_ascension_thursday, _add_holiday_40_days_past_easter, _add_whit_monday,
etc.), causing PUBLIC dates to leak into SCHOOL and duplicate entries; remove
those duplicated _add_* public-holiday calls from the SCHOOL-specific blocks and
instead register only school-specific closures (break periods) and true
community-specific school days (e.g., Sep 27 for WBR, Nov 15 for GER, Dec 24 for
GER if intended) so SCHOOL only contains non-public school closures; ensure you
rely on _populate_public_holidays for public dates and do not re-add them (also
apply the same fix to the other occurrences referenced in the file around the
other ranges).


# Summer holidays.
Comment thread
KJhellico marked this conversation as resolved.
Outdated
start = date(year, JUL, 1)
end = date(year, SEP, 1)
self._add_multiday_holiday(start, (end - start).days, name=tr("Zomervakantie"))
Comment thread
KJhellico marked this conversation as resolved.
Outdated

# Autumn holidays (week of 1 November).
nov_1 = date(year, NOV, 1)
start = _get_nth_weekday_from(-1 if nov_1.weekday() <= 5 else 1, 0, nov_1)
self._add_multiday_holiday(start, 7, name=tr("Herfstvakantie"))
Comment thread
KJhellico marked this conversation as resolved.
Outdated

# Armistice Day (End of World War I).
Comment thread
KJhellico marked this conversation as resolved.
Outdated
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.
herfst_start = _get_nth_weekday_of_month(3, MON, OCT, year)
self._add_multiday_holiday(herfst_start, 14, name=tr("Herfstvakantie"))

# current year's Christmas.
christmas = date(year, DEC, 25)
kerst_start = _get_nth_weekday_from(-1 if christmas.weekday() <= 4 else 1, 0, christmas)
self._add_multiday_holiday(kerst_start, 14, name=tr("Kerstvakantie"))

# Previous year's Christmas (for January spillover).
prev_christmas = date(year - 1, DEC, 25)
prev_start = _get_nth_weekday_from(
-1 if prev_christmas.weekday() <= 4 else 1,
0,
prev_christmas,
)
self._add_multiday_holiday(prev_start, 14, name=tr("Kerstvakantie"))

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

# Spring Holidays.
paas_start = _get_nth_weekday_of_month(4, MON, APR, year)
self._add_multiday_holiday(paas_start, 14, name=tr("Paasvakantie"))

# Summer holidays.
zomer_start = _get_nth_weekday_of_month(1, SAT, JUL, year)

# School restart.
school_restart = _get_nth_weekday_of_month(4, MON, AUG, year)

# Summer ends day before school restart.
zomer_end = _timedelta(school_restart, -1)

duration = (zomer_end - zomer_start).days + 1

self._add_multiday_holiday(
zomer_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))

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the existing _add_holiday_* helpers instead of constructing dates manually.

self._add_holiday(tr("..."), date(year, SEP, 27)) can be replaced with self._add_holiday_sep_27(tr("...")), and likewise _add_holiday_nov_15 and _add_holiday_dec_24. The helper form is the project's idiomatic style and avoids the manual date(year, ...) boilerplate.

♻️ Suggested fix
-        self._add_holiday(tr("Feestdag van de Franse Gemeenschap"), date(year, SEP, 27))
+        self._add_holiday_sep_27(tr("Feestdag van de Franse Gemeenschap"))
-        self._add_holiday(tr("Feestdag van de Duitstalige Gemeenschap"), date(year, NOV, 15))
+        self._add_holiday_nov_15(tr("Feestdag van de Duitstalige Gemeenschap"))
-        self._add_holiday(tr("Heiligavond"), date(year, DEC, 24))
+        self._add_holiday_dec_24(tr("Heiligavond"))

Also applies to: 377-377, 389-389

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@holidays/countries/belgium.py` at line 281, Replace manual date constructions
that call self._add_holiday(tr("..."), date(year, SEP, 27/ NOV, 15/ DEC, 24))
with the project's helper methods: use self._add_holiday_sep_27(tr("...")),
self._add_holiday_nov_15(tr("...")), and self._add_holiday_dec_24(tr("..."))
respectively; locate the three occurrences where date(year, SEP, 27), date(year,
NOV, 15), and date(year, DEC, 24) are passed into self._add_holiday and swap
them to the corresponding _add_holiday_* helper calls to follow the idiomatic
style and remove manual date boilerplate.


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

# Ascension Day.
self._add_ascension_thursday(tr("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 (week of 25 December).
christmas = date(year, DEC, 25)
# 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.
start = _get_nth_weekday_from(-1 if christmas.weekday() <= 4 else 1, 0, christmas)
self._add_multiday_holiday(start, 13, name=tr("Kerstvakantie"))

# Previous year's Christmas (for January spillover).
prev_christmas = date(year - 1, DEC, 25)
prev_start = _get_nth_weekday_from(
-1 if prev_christmas.weekday() <= 4 else 1,
0,
prev_christmas,
)
self._add_multiday_holiday(prev_start, 13, name=tr("Kerstvakantie"))

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

# Easter holidays.
# - March Easter → week before Easter.
# - Easter after 15 April → second Monday before Easter.
# - Otherwise → first Monday of April.
paas_start = _timedelta(easter, 1) # Monday after Easter
self._add_multiday_holiday(paas_start, 13, name=tr("Paasvakantie"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Summer holidays.
start = date(year, JUN, 30)
end = date(year, SEP, 1)
self._add_multiday_holiday(start, (end - start).days, name=tr("Zomervakantie"))

# Autumn holidays (week of 1 November).
nov_1 = date(year, NOV, 1)
start = _get_nth_weekday_from(-1 if nov_1.weekday() <= 5 else 1, 0, nov_1)
self._add_multiday_holiday(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("Hemelvaart"))

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

# Christmas Eve (observed in German-speaking schools).
self._add_holiday(tr("Heiligavond"), date(year, DEC, 24))
Comment on lines +388 to +389

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Christmas Eve is already inside the GER Christmas break.

Lines 327–337 register the Christmas break which spans the week of Dec 25 (and thus includes Dec 24 in most years). Re-adding tr("Heiligavond") on Dec 24 will either collide with the existing break entry or shadow it depending on insertion order. If Dec 24 is meant to be a distinct named day for GER, add it before the break and document the override; otherwise drop it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@holidays/countries/belgium.py` around lines 388 - 389, The explicit addition
of Heiligavond via self._add_holiday(tr("Heiligavond"), date(year, DEC, 24))
conflicts with the previously added Christmas break entry (the block that
registers the Christmas break spanning Dec 25 and surrounding days); either
remove this Heiligavond line to avoid collision, or if you intend Dec 24 to be a
distinct named day for Belgium, move the self._add_holiday(tr("Heiligavond"),
date(year, DEC, 24)) call so it executes before the Christmas-break registration
and add a comment documenting that it intentionally overrides/shadows the break
entry for 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