diff --git a/README.md b/README.md
index f38d2d9a29..d959c643f7 100644
--- a/README.md
+++ b/README.md
@@ -310,9 +310,9 @@ any) in brackets, available languages and additional holiday categories. All cou
| Belgium |
BE |
- |
+Regions: VLG, WBR, GER |
de, en_US, fr, nl, uk |
-BANK |
+BANK, SCHOOL |
| Belize |
diff --git a/docs/holiday_categories.md b/docs/holiday_categories.md
index 4bd1506a9f..7ccb128a7e 100644
--- a/docs/holiday_categories.md
+++ b/docs/holiday_categories.md
@@ -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="WBR", categories=SCHOOL, years=2025)
+for date, name in sorted(be_flemish_schools.items()):
+ print(date, name)
+```
+
### Unofficial Holidays Example
Get unofficial holidays in the United States:
@@ -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}")
```
diff --git a/holidays/countries/belgium.py b/holidays/countries/belgium.py
index c4473459d1..0d63f6a042 100644
--- a/holidays/countries/belgium.py
+++ b/holidays/countries/belgium.py
@@ -10,28 +10,62 @@
# Website: https://github.com/vacanza/holidays
# License: MIT (see LICENSE file)
+from datetime import date, timedelta
from gettext import gettext as tr
-from holidays.constants import BANK, PUBLIC
+from holidays.calendars.gregorian import (
+ JAN,
+ APR,
+ JUN,
+ JUL,
+ AUG,
+ SEP,
+ 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:
*
*
*
*
+ * 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)
@@ -84,6 +118,276 @@ def _populate_bank_holidays(self):
# 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
+
+ # Christmas Break:
+ # Starts on the Monday of the week containing Christmas,
+ # or on the following Monday if Christmas falls on a weekend.
+ # The break lasts two weeks (14 days) and spans December and January,
+ # therefore the January portion is added separately.
+
+ # Christmas Break.
+ name = tr("Kerstvakantie")
+
+ # December portion of the current Christmas Break.
+ 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)
+
+ # January portion of the previous Christmas Break.
+ new_year = date(year, JAN, 1)
+ january_duration = (4 - new_year.weekday()) % 7 + 3
+ self._add_multiday_holiday(new_year, january_duration, name=name)
+
+ # Carnival Break:
+ # Starts on Carnival Monday, 48 days before Easter.
+ # The break lasts one week (7 days).
+
+ # Carnival Break.
+ self._add_multiday_holiday(_timedelta(easter, -48), 7, name=tr("Krokusvakantie"))
+
+ # Easter Break:
+ # The start date depends on the date of Easter:
+ # - If Easter falls in March, the break starts on the Monday after Easter.
+ # - If Easter falls on or after April 16, the break starts 13 days before Easter.
+ # - Otherwise, the break starts on the first Monday of April.
+ # The break normally lasts two weeks (14 days), extended to include Easter Monday
+ # when necessary.
+ easter_start = _get_nth_weekday_of_month(1, MON, APR, year)
+ easter_duration = 14
+ if easter.month == 3:
+ easter_start = _timedelta(easter, +1)
+ elif easter.day >= 16:
+ easter_start = _timedelta(easter, -13)
+ easter_duration = 15
+ # Easter Break.
+ 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 Day.
+ self._add_holiday_40_days_past_easter(tr("Vrijdag na O. L. H. Hemelvaart"))
+
+ # Whit Monday.
+ self._add_whit_monday(tr("Pinkstermaandag"))
+
+ # Summer Break:
+ # Fixed period running from July 1 through August 31.
+ # The break lasts 62 days.
+
+ # Summer Break.
+ self._add_multiday_holiday(date(year, JUL, 1), 62, name=tr("Zomervakantie"))
+
+ # Autumn Break:
+ # Starts on the Monday of the week containing November 1,
+ # or on the following Monday if November 1 falls on Sunday.
+ # The break lasts one week (7 days).
+
+ nov_1 = date(year, NOV, 1)
+ autumn_start = _get_nth_weekday_from(1 if self._is_sunday(nov_1) else -1, MON, nov_1)
+ # Autumn Break.
+ self._add_multiday_holiday(autumn_start, 7, name=tr("Herfstvakantie"))
+
+ # Armistice Day.
+ self._add_remembrance_day(tr("Wapenstilstand"))
+
+ def _populate_subdiv_wbr_school_holidays(self):
+ """
+ School holidays for the French Community (Wallonie-Bruxelles).
+ Based on the official compulsory education calendar.
+ """
+
+ if self.categories != {SCHOOL}:
+ return
+ year = self._year
+ easter = self._easter_sunday
+
+ # Christmas Holidays.
+ # Start on the Monday of the week containing Christmas
+ # (or the following Monday if Christmas falls on a weekend).
+ # The holiday period spans late December and continues into January,
+ # with the January portion added separately.
+ name = tr("Kerstvakantie")
+
+ # Present year's Christmas Holidays.
+ 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).
+ new_year = date(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)
+
+ # Carnival Holidays:
+ # Start on Carnival Monday (48 days before Easter).
+ # Duration is two weeks.
+
+ self._add_multiday_holiday(_timedelta(easter, days=-48), 14, name=tr("Krokusvakantie"))
+
+ # Spring Holidays (WBR, post-2022):
+ # Start on the last Monday of April.
+ # If this falls before April 25, shift to the following Monday (in May).
+
+ apr_30 = date(year, APR, 30)
+
+ spring_start = _get_nth_weekday_from(-1, MON, apr_30)
+
+ if spring_start.day < 25:
+ spring_start += timedelta(days=7)
+ self._add_multiday_holiday(spring_start, 14, name=tr("Lentevakantie"))
+
+ # Summer holidays.
+ summer_start = _get_nth_weekday_of_month(1, SAT, JUL, year)
+ # If the first Saturday is July 1 (too early), shift to the next Saturday.
+ if summer_start.day == 1:
+ summer_start += timedelta(days=7)
+ 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"))
+
+ # Autumn Holidays:
+ # - Start → second last Monday of October.
+ # - Before the 2022-2023 school year → duration was 1 week.
+ # - From the 2022-2023 school year → duration is 2 weeks.
+
+ # Autumn Holidays.
+ nov_first = date(year, 11, 1)
+ last_monday = nov_first - timedelta(days=(nov_first.weekday() - MON) % 7)
+ autumn_start = last_monday - timedelta(days=7)
+ if year >= 2022:
+ self._add_multiday_holiday(autumn_start, 14, name=tr("Herfstvakantie"))
+ else:
+ self._add_multiday_holiday(autumn_start, 7, name=tr("Herfstvakantie"))
+
+ 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 patterns similar to other Belgian communities,
+ with some variations in timing and duration.
+ """
+
+ if self.categories != {SCHOOL}:
+ return
+ year = self._year
+ easter = self._easter_sunday
+
+ # Christmas Holidays.
+ # Start on the Monday of the week containing Christmas
+ # (or the following Monday if Christmas falls on a weekend).
+ # The holiday period spans late December and continues into January,
+ # with the January portion added separately.
+ name = tr("Kerstvakantie")
+
+ # Present year's Christmas Holidays.
+ 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).
+ new_year = date(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)
+
+ # Carnival Holidays.
+ # Start on Carnival Monday, which occurs 48 days before Easter.
+ # The vacation lasts for one week.
+ self._add_multiday_holiday(_timedelta(easter, days=-48), 7, name=tr("Krokusvakantie"))
+
+ # Easter Holidays.
+ # Usually start on the first Monday of April.
+ # If Easter is very late (on or after April 18), holidays start on the Monday after Easter.
+ # Duration is approximately two weeks (12 days).
+ april_1 = date(year, APR, 1)
+ first_monday_april = _get_nth_weekday_from(1, MON, april_1)
+
+ # If Easter is very late → shift after Easter
+ if easter.month == 4 and easter.day >= 18:
+ easter_start = _timedelta(easter, days=1)
+ else:
+ easter_start = first_monday_april
+
+ # Duration is approximately two weeks (12 calendar days).
+ self._add_multiday_holiday(easter_start, 12, name=tr("Paasvakantie"))
+
+ # Summer holidays.
+ self._add_multiday_holiday(date(year, JUN, 30), 63, name=tr("Zomervakantie"))
+
+ # Autumn Holidays.
+ # Start on the Monday of the week containing November 1.
+ # Duration is one week in the German-speaking Community calendar.
+ 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"))
+
+ # 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
diff --git a/holidays/locale/de/LC_MESSAGES/BE.po b/holidays/locale/de/LC_MESSAGES/BE.po
index e467e0f6c1..df1f07453a 100644
--- a/holidays/locale/de/LC_MESSAGES/BE.po
+++ b/holidays/locale/de/LC_MESSAGES/BE.po
@@ -87,3 +87,39 @@ 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"
+
+#. Spring Day.
+msgid "Lentevakantie"
+msgstr "Frühlingsferien"
diff --git a/holidays/locale/en_US/LC_MESSAGES/BE.po b/holidays/locale/en_US/LC_MESSAGES/BE.po
index 6d499a4560..1c6c81c6d6 100644
--- a/holidays/locale/en_US/LC_MESSAGES/BE.po
+++ b/holidays/locale/en_US/LC_MESSAGES/BE.po
@@ -87,3 +87,39 @@ msgstr "Friday after Ascension Day"
#. Bank Holiday.
msgid "Banksluitingsdag"
msgstr "Bank Holiday"
+
+#. Christmas Holidays.
+msgid "Kerstvakantie"
+msgstr "Christmas Holidays"
+
+#. Carnival Holidays.
+msgid "Krokusvakantie"
+msgstr "Carnival Holidays"
+
+#. Easter Holidays.
+msgid "Paasvakantie"
+msgstr "Easter Holidays"
+
+#. Summer Holidays.
+msgid "Zomervakantie"
+msgstr "Summer Holidays"
+
+#. Autumn Holidays.
+msgid "Herfstvakantie"
+msgstr "Autumn Holidays"
+
+#. French Community Day.
+msgid "Feestdag van de Franse Gemeenschap"
+msgstr "French Community Day"
+
+#. German Community Day.
+msgid "Feestdag van de Duitstalige Gemeenschap"
+msgstr "German-speaking Community Day"
+
+#. Christmas Eve.
+msgid "Heiligavond"
+msgstr "Christmas Eve"
+
+#. Spring Holidays.
+msgid "Lentevakantie"
+msgstr "Spring Holidays"
diff --git a/holidays/locale/fr/LC_MESSAGES/BE.po b/holidays/locale/fr/LC_MESSAGES/BE.po
index 1dbb1eb70d..b6a8397e7b 100644
--- a/holidays/locale/fr/LC_MESSAGES/BE.po
+++ b/holidays/locale/fr/LC_MESSAGES/BE.po
@@ -87,3 +87,39 @@ msgstr "Vendredi suivant l'Ascension"
#. Bank Holiday.
msgid "Banksluitingsdag"
msgstr "Jour de fermeture bancaire"
+
+#. Christmas Holidays.
+msgid "Kerstvakantie"
+msgstr "Vacances de Noël"
+
+#. Carnival Holidays.
+msgid "Krokusvakantie"
+msgstr "Vacances de carnaval"
+
+#. Easter Holidays.
+msgid "Paasvakantie"
+msgstr "Vacances de Pâques"
+
+#. Summer Holidays.
+msgid "Zomervakantie"
+msgstr "Vacances d'été"
+
+#. Autumn Holidays.
+msgid "Herfstvakantie"
+msgstr "Vacances d'automne"
+
+#. French Community Day.
+msgid "Feestdag van de Franse Gemeenschap"
+msgstr "Fête de la Communauté française"
+
+#. German Community Day.
+msgid "Feestdag van de Duitstalige Gemeenschap"
+msgstr "Fête de la Communauté germanophone"
+
+#. Christmas Eve.
+msgid "Heiligavond"
+msgstr "Réveillon de Noël"
+
+#. Spring Holidays.
+msgid "Lentevakantie"
+msgstr "Vacances de printemps"
diff --git a/holidays/locale/nl/LC_MESSAGES/BE.po b/holidays/locale/nl/LC_MESSAGES/BE.po
index a55e83d6ea..1106a79bae 100644
--- a/holidays/locale/nl/LC_MESSAGES/BE.po
+++ b/holidays/locale/nl/LC_MESSAGES/BE.po
@@ -87,3 +87,39 @@ msgstr ""
#. Bank Holiday.
msgid "Banksluitingsdag"
msgstr ""
+
+#. Christmas Holidays.
+msgid "Kerstvakantie"
+msgstr ""
+
+#. Carnival Holidays.
+msgid "Krokusvakantie"
+msgstr ""
+
+#. Easter Holidays.
+msgid "Paasvakantie"
+msgstr ""
+
+#. Summer Holidays.
+msgid "Zomervakantie"
+msgstr ""
+
+#. Autumn Holidays.
+msgid "Herfstvakantie"
+msgstr ""
+
+#. French Community Day.
+msgid "Feestdag van de Franse Gemeenschap"
+msgstr ""
+
+#. German-speaking Community Day.
+msgid "Feestdag van de Duitstalige Gemeenschap"
+msgstr ""
+
+#. Christmas Eve.
+msgid "Heiligavond"
+msgstr ""
+
+#. Spring Holidays.
+msgid "Lentevakantie"
+msgstr ""
diff --git a/holidays/locale/uk/LC_MESSAGES/BE.po b/holidays/locale/uk/LC_MESSAGES/BE.po
index 7ba38640b4..3f98ed97eb 100644
--- a/holidays/locale/uk/LC_MESSAGES/BE.po
+++ b/holidays/locale/uk/LC_MESSAGES/BE.po
@@ -87,3 +87,39 @@ msgstr "Пʼятниця після Вознесіння Господнього"
#. Bank Holiday.
msgid "Banksluitingsdag"
msgstr "Банківський вихідний"
+
+#. Christmas Holidays.
+msgid "Kerstvakantie"
+msgstr "Різдвяні канікули"
+
+#. Carnival Holidays.
+msgid "Krokusvakantie"
+msgstr "Карнавальні канікули"
+
+#. Easter Holidays.
+msgid "Paasvakantie"
+msgstr "Великодні канікули"
+
+#. Summer Holidays.
+msgid "Zomervakantie"
+msgstr "Літні канікули"
+
+#. Autumn Holidays.
+msgid "Herfstvakantie"
+msgstr "Осінні канікули"
+
+#. French Community Day.
+msgid "Feestdag van de Franse Gemeenschap"
+msgstr "День Французької спільноти"
+
+#. German-speaking Community Day.
+msgid "Feestdag van de Duitstalige Gemeenschap"
+msgstr "День Німецькомовної спільноти"
+
+#. Christmas Eve.
+msgid "Heiligavond"
+msgstr "Святвечір"
+
+#. Spring Holidays.
+msgid "Lentevakantie"
+msgstr "Весняні канікули"
diff --git a/tests/countries/test_belgium.py b/tests/countries/test_belgium.py
index b54c8a08b3..ab45c0b342 100644
--- a/tests/countries/test_belgium.py
+++ b/tests/countries/test_belgium.py
@@ -12,6 +12,7 @@
from unittest import TestCase
+from holidays.constants import SCHOOL
from holidays.countries.belgium import Belgium
from tests.common import CommonCountryTests
@@ -299,3 +300,48 @@ def test_l10n_uk(self):
("2022-12-25", "Різдво Христове"),
("2022-12-26", "Банківський вихідний"),
)
+
+ def test_vlg_summer_holidays(self):
+ name = "Zomervakantie"
+
+ self.assertHolidayName(
+ name,
+ Belgium(subdiv="VLG", categories=SCHOOL),
+ "2025-07-05",
+ "2024-07-06",
+ "2023-07-01",
+ "2022-08-07",
+ "2021-08-18",
+ )
+
+ def test_wbr_summer_holidays(self):
+ name = "Zomervakantie"
+
+ self.assertHolidayName(
+ name,
+ Belgium(subdiv="WBR", categories=SCHOOL),
+ "2025-08-11",
+ "2024-07-06",
+ "2023-07-26",
+ "2022-08-07",
+ "2021-08-18",
+ )
+
+ def test_ger_summer_holidays(self):
+ name = "Zomervakantie"
+
+ self.assertHolidayName(
+ name,
+ Belgium(subdiv="GER", categories=SCHOOL),
+ "2025-07-07",
+ "2024-08-30",
+ "2023-07-20",
+ "2022-07-10",
+ "2022-07-14",
+ "2021-08-04",
+ )
+
+ def test_school_not_in_public(self):
+
+ self.assertNoHoliday("2024-07-06")
+ self.assertNoHoliday("2024-05-02")