Repository: pandas-dev/pandas (python)

Issue:
DOC/ENH: `Holiday` documentation is missing or hard to find for re-arranged national holidays. 
### Pandas version checks

- [X] I have checked that the issue still exists on the latest versions of the docs on `main` [here](https://pandas.pydata.org/docs/dev/)


### Location of the documentation

https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#holidays-holiday-calendars

### Documentation problem

I am creating a financial calendar using pandas holiday calendar functions: `CustomBusinessDay` and `AbstractHolidayCalendar` from a set of `rules`.

In UK the spring bank holiday is defined as the last Monday in May which we can code as a rule object:

```python
Holiday("UK Spring Bank Holiday", month=5, day=31, offset=DateOffset(weekday=MO(-1)))
```
In 2022, to commemorate the Queen's Platinum Jubilee this holiday was moved to Thursday 2nd June and an extra holiday added on Friday 3rd June. 

Adding an extra holiday to the rules is easy..
```python
Holiday("Queen Jubilee Fri", year=2022, month=6, day=3)
```

I wondered if there were any options to the `Holiday` class that allowed excluding a list of dates from the observance. It was very difficult to find any documentation on the class and I had to resort to looking at the code.

In the code the class is poorly documented and has inaccuracies. 

There was also no ability to do what I was hoping. The applied solution was:

```python
rules = [
    Holiday("UK Spring Hol pre Jubilee", end_date=datetime(2022, 5, 1), month=5, day=31, offset=DateOffset(weekday=MO(-1))),
    Holiday("Queen Jubilee Thu", year=2022, month=6, day=2),
    Holiday("Queen Jubilee Fri", year=2022, month=6, day=3),
    Holiday("UK Spring Hol post Jubilee", start_date=datetime(2022, 7, 1), month=5, day=31, offset=DateOffset(weekday=MO(-1))),
]

### Suggested fix for documentation

.

Pull request:
DOC/ENH: Holiday exclusion argument
- [x] closes #54382 (Replace xxxx with the GitHub issue number)
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Patch:
diff --git a/ci/deps/actions-310-minimum_versions.yaml b/ci/deps/actions-310-minimum_versions.yaml
index eaba29c2f7..9f12fe941d 100644
--- a/ci/deps/actions-310-minimum_versions.yaml
+++ b/ci/deps/actions-310-minimum_versions.yaml
@@ -18,7 +18,7 @@ dependencies:
   - pytest-xdist>=3.4.0
   - pytest-localserver>=0.8.1
   - pytest-qt>=4.4.0
-  - boto3
+  - boto3=1.37.3
 
   # required dependencies
   - python-dateutil=2.8.2
diff --git a/ci/deps/actions-310.yaml b/ci/deps/actions-310.yaml
index fbeabb56a6..66d49475bf 100644
--- a/ci/deps/actions-310.yaml
+++ b/ci/deps/actions-310.yaml
@@ -16,7 +16,7 @@ dependencies:
   - pytest-xdist>=3.4.0
   - pytest-localserver>=0.8.1
   - pytest-qt>=4.4.0
-  - boto3
+  - boto3=1.37.3
 
   # required dependencies
   - python-dateutil
diff --git a/ci/deps/actions-311-downstream_compat.yaml b/ci/deps/actions-311-downstream_compat.yaml
index 07f8658685..100a250f0b 100644
--- a/ci/deps/actions-311-downstream_compat.yaml
+++ b/ci/deps/actions-311-downstream_compat.yaml
@@ -17,7 +17,7 @@ dependencies:
   - pytest-xdist>=3.4.0
   - pytest-localserver>=0.8.1
   - pytest-qt>=4.4.0
-  - boto3
+  - boto3=1.37.3
 
   # required dependencies
   - python-dateutil
diff --git a/ci/deps/actions-311.yaml b/ci/deps/actions-311.yaml
index 9ab82d6950..9669c1e29a 100644
--- a/ci/deps/actions-311.yaml
+++ b/ci/deps/actions-311.yaml
@@ -16,7 +16,7 @@ dependencies:
   - pytest-xdist>=3.4.0
   - pytest-localserver>=0.8.1
   - pytest-qt>=4.4.0
-  - boto3
+  - boto3=1.37.3
 
   # required dependencies
   - python-dateutil
diff --git a/ci/deps/actions-312.yaml b/ci/deps/actions-312.yaml
index d12fe4f7d6..61f1d602bb 100644
--- a/ci/deps/actions-312.yaml
+++ b/ci/deps/actions-312.yaml
@@ -16,7 +16,7 @@ dependencies:
   - pytest-xdist>=3.4.0
   - pytest-localserver>=0.8.1
   - pytest-qt>=4.4.0
-  - boto3
+  - boto3=1.37.3
 
   # required dependencies
   - python-dateutil
diff --git a/ci/deps/actions-313.yaml b/ci/deps/actions-313.yaml
index 57ed0ea062..11f4428be2 100644
--- a/ci/deps/actions-313.yaml
+++ b/ci/deps/actions-313.yaml
@@ -16,7 +16,7 @@ dependencies:
   - pytest-xdist>=3.4.0
   - pytest-localserver>=0.8.1
   - pytest-qt>=4.4.0
-  - boto3
+  - boto3=1.37.3
 
   # required dependencies
   - python-dateutil
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 03a3867083..5160d2ea8b 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -64,6 +64,7 @@ Other enhancements
 - :meth:`Series.nlargest` uses a 'stable' sort internally and will preserve original ordering.
 - :class:`ArrowDtype` now supports ``pyarrow.JsonType`` (:issue:`60958`)
 - :class:`DataFrameGroupBy` and :class:`SeriesGroupBy` methods ``sum``, ``mean``, ``median``, ``prod``, ``min``, ``max``, ``std``, ``var`` and ``sem`` now accept ``skipna`` parameter (:issue:`15675`)
+- :class:`Holiday` has gained the constructor argument and field ``exclude_dates`` to exclude specific datetimes from a custom holiday calendar (:issue:`54382`)
 - :class:`Rolling` and :class:`Expanding` now support ``nunique`` (:issue:`26958`)
 - :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`)
 - :func:`read_parquet` accepts ``to_pandas_kwargs`` which are forwarded to :meth:`pyarrow.Table.to_pandas` which enables passing additional keywords to customize the conversion to pandas, such as ``maps_as_pydicts`` to read the Parquet map data type as python dictionaries (:issue:`56842`)
diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py
index 2d195fbbc4..8ad2541d2e 100644
--- a/pandas/tseries/holiday.py
+++ b/pandas/tseries/holiday.py
@@ -169,6 +169,7 @@ class Holiday:
         start_date=None,
         end_date=None,
         days_of_week: tuple | None = None,
+        exclude_dates: DatetimeIndex | None = None,
     ) -> None:
         """
         Parameters
@@ -193,6 +194,8 @@ class Holiday:
         days_of_week : tuple of int or dateutil.relativedelta weekday strs, default None
             Provide a tuple of days e.g  (0,1,2,3,) for Monday Through Thursday
             Monday=0,..,Sunday=6
+        exclude_dates : DatetimeIndex or default None
+            Specific dates to exclude e.g. skipping a specific year's holiday
 
         Examples
         --------
@@ -257,6 +260,9 @@ class Holiday:
         self.observance = observance
         assert days_of_week is None or type(days_of_week) == tuple
         self.days_of_week = days_of_week
+        if not (exclude_dates is None or isinstance(exclude_dates, DatetimeIndex)):
+            raise ValueError("exclude_dates must be None or of type DatetimeIndex.")
+        self.exclude_dates = exclude_dates
 
     def __repr__(self) -> str:
         info = ""
@@ -328,6 +334,9 @@ class Holiday:
         holiday_dates = holiday_dates[
             (holiday_dates >= filter_start_date) & (holiday_dates <= filter_end_date)
         ]
+
+        if self.exclude_dates is not None:
+            holiday_dates = holiday_dates.difference(self.exclude_dates)
         if return_name:
             return Series(self.name, index=holiday_dates)
         return holiday_dates


Test patch:
diff --git a/pandas/tests/generic/test_to_xarray.py b/pandas/tests/generic/test_to_xarray.py
index 8917e4e3f3..5f805ab37c 100644
--- a/pandas/tests/generic/test_to_xarray.py
+++ b/pandas/tests/generic/test_to_xarray.py
@@ -93,6 +93,7 @@ class TestSeriesToXArray:
             isinstance(index.dtype, StringDtype)
             and index.dtype.storage == "pyarrow"
             and Version(xarray.__version__) > Version("2024.9.0")
+            and Version(xarray.__version__) < Version("2025.6.0")
         ):
             request.applymarker(
                 pytest.mark.xfail(
diff --git a/pandas/tests/tseries/holiday/test_holiday.py b/pandas/tests/tseries/holiday/test_holiday.py
index ffe6ff0b51..37e029f31b 100644
--- a/pandas/tests/tseries/holiday/test_holiday.py
+++ b/pandas/tests/tseries/holiday/test_holiday.py
@@ -353,3 +353,104 @@ def test_holidays_with_timezone_specified_but_no_occurences():
     expected_results.index = expected_results.index.as_unit("ns")
 
     tm.assert_equal(test_case, expected_results)
+
+
+def test_holiday_with_exclusion():
+    # GH 54382
+    start = Timestamp("2020-05-01")
+    end = Timestamp("2025-05-31")
+    exclude = DatetimeIndex([Timestamp("2022-05-30")])  # Queen's platinum Jubilee
+
+    queens_jubilee_uk_spring_bank_holiday: Holiday = Holiday(
+        "Queen's Jubilee UK Spring Bank Holiday",
+        month=5,
+        day=31,
+        offset=DateOffset(weekday=MO(-1)),
+        exclude_dates=exclude,
+    )
+
+    result = queens_jubilee_uk_spring_bank_holiday.dates(start, end)
+    expected = DatetimeIndex(
+        [
+            Timestamp("2020-05-25"),
+            Timestamp("2021-05-31"),
+            Timestamp("2023-05-29"),
+            Timestamp("2024-05-27"),
+            Timestamp("2025-05-26"),
+        ],
+        dtype="datetime64[ns]",
+    )
+    tm.assert_index_equal(result, expected)
+
+
+def test_holiday_with_multiple_exclusions():
+    start = Timestamp("2025-01-01")
+    end = Timestamp("2065-12-31")
+    exclude = DatetimeIndex(
+        [
+            Timestamp("2025-01-01"),
+            Timestamp("2042-01-01"),
+            Timestamp("2061-01-01"),
+        ]
+    )  # Yakudoshi new year
+
+    yakudoshi_new_year: Holiday = Holiday(
+        "Yakudoshi New Year", month=1, day=1, exclude_dates=exclude
+    )
+
+    result = yakudoshi_new_year.dates(start, end)
+    expected = DatetimeIndex(
+        [
+            Timestamp("2026-01-01"),
+            Timestamp("2027-01-01"),
+            Timestamp("2028-01-01"),
+            Timestamp("2029-01-01"),
+            Timestamp("2030-01-01"),
+            Timestamp("2031-01-01"),
+            Timestamp("2032-01-01"),
+            Timestamp("2033-01-01"),
+            Timestamp("2034-01-01"),
+            Timestamp("2035-01-01"),
+            Timestamp("2036-01-01"),
+            Timestamp("2037-01-01"),
+            Timestamp("2038-01-01"),
+            Timestamp("2039-01-01"),
+            Timestamp("2040-01-01"),
+            Timestamp("2041-01-01"),
+            Timestamp("2043-01-01"),
+            Timestamp("2044-01-01"),
+            Timestamp("2045-01-01"),
+            Timestamp("2046-01-01"),
+            Timestamp("2047-01-01"),
+            Timestamp("2048-01-01"),
+            Timestamp("2049-01-01"),
+            Timestamp("2050-01-01"),
+            Timestamp("2051-01-01"),
+            Timestamp("2052-01-01"),
+            Timestamp("2053-01-01"),
+            Timestamp("2054-01-01"),
+            Timestamp("2055-01-01"),
+            Timestamp("2056-01-01"),
+            Timestamp("2057-01-01"),
+            Timestamp("2058-01-01"),
+            Timestamp("2059-01-01"),
+            Timestamp("2060-01-01"),
+            Timestamp("2062-01-01"),
+            Timestamp("2063-01-01"),
+            Timestamp("2064-01-01"),
+            Timestamp("2065-01-01"),
+        ],
+        dtype="datetime64[ns]",
+    )
+    tm.assert_index_equal(result, expected)
+
+
+def test_exclude_date_value_error():
+    msg = "exclude_dates must be None or of type DatetimeIndex."
+
+    with pytest.raises(ValueError, match=msg):
+        exclude = [
+            Timestamp("2025-06-10"),
+            Timestamp("2026-06-10"),
+        ]
+        Holiday("National Ice Tea Day", month=6, day=10, exclude_dates=exclude)