From 2af8549e52a3244914d761426b7cf277aefe48dd Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 16 Jul 2025 09:41:22 -0700 Subject: [PATCH 1/3] API: IncompatibleFrequency subclass TypeError --- doc/source/reference/testing.rst | 1 + doc/source/whatsnew/v3.0.0.rst | 1 + pandas/_libs/tslibs/period.pyi | 2 +- pandas/_libs/tslibs/period.pyx | 2 +- pandas/core/arrays/datetimelike.py | 4 ++-- pandas/core/indexes/base.py | 3 +-- pandas/errors/__init__.py | 2 ++ pandas/tests/indexes/period/test_indexing.py | 2 +- pandas/tests/indexes/period/test_join.py | 10 ++++------ pandas/tests/indexes/period/test_period.py | 4 +++- pandas/tests/series/test_arithmetic.py | 5 ----- 11 files changed, 17 insertions(+), 19 deletions(-) diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index 1f164d1aa98b4..2c9c2dcae0f69 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -36,6 +36,7 @@ Exceptions and warnings errors.DuplicateLabelError errors.EmptyDataError errors.IncompatibilityWarning + errors.IncompatibleFrequency errors.IndexingError errors.InvalidColumnName errors.InvalidComparison diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 977186d808e81..3e47789568296 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -414,6 +414,7 @@ Other API changes - Index set operations (like union or intersection) will now ignore the dtype of an empty ``RangeIndex`` or empty ``Index`` with object dtype when determining the dtype of the resulting Index (:issue:`60797`) +- :class:`IncompatibleFrequency` now subclasses ``TypeError`` instead of ``ValueError``. As a result, joins with mismatched frequencies now cast to object like other non-comparable joins, and arithmetic with indexes with mismatched frequencies align (:issue:`55782`) .. --------------------------------------------------------------------------- .. _whatsnew_300.deprecations: diff --git a/pandas/_libs/tslibs/period.pyi b/pandas/_libs/tslibs/period.pyi index 22f3bdbe668de..5cb9f891b312a 100644 --- a/pandas/_libs/tslibs/period.pyi +++ b/pandas/_libs/tslibs/period.pyi @@ -15,7 +15,7 @@ from pandas._typing import ( INVALID_FREQ_ERR_MSG: str DIFFERENT_FREQ: str -class IncompatibleFrequency(ValueError): ... +class IncompatibleFrequency(TypeError): ... def periodarr_to_dt64arr( periodarr: npt.NDArray[np.int64], # const int64_t[:] diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 350216cf89ce4..740f8d7b54f3b 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1625,7 +1625,7 @@ DIFFERENT_FREQ = ("Input has different freq={other_freq} " "from {cls}(freq={own_freq})") -class IncompatibleFrequency(ValueError): +class IncompatibleFrequency(TypeError): pass diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 9a723a88941b6..4b82e61f47d59 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -544,7 +544,7 @@ def _validate_comparison_value(self, other): other = self._scalar_type(other) try: self._check_compatible_with(other) - except (TypeError, IncompatibleFrequency) as err: + except TypeError as err: # e.g. tzawareness mismatch raise InvalidComparison(other) from err @@ -558,7 +558,7 @@ def _validate_comparison_value(self, other): try: other = self._validate_listlike(other, allow_object=True) self._check_compatible_with(other) - except (TypeError, IncompatibleFrequency) as err: + except TypeError as err: if is_object_dtype(getattr(other, "dtype", None)): # We will have to operate element-wise pass diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 0b719ae21d5b9..a9e375021da27 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -38,7 +38,6 @@ no_default, ) from pandas._libs.tslibs import ( - IncompatibleFrequency, OutOfBoundsDatetime, Timestamp, tz_compare, @@ -3143,7 +3142,7 @@ def _union(self, other: Index, sort: bool | None): # test_union_same_value_duplicated_in_both fails) try: return self._outer_indexer(other)[0] - except (TypeError, IncompatibleFrequency): + except TypeError: # incomparable objects; should only be for object dtype value_list = list(lvals) diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index d1ca056ffcb19..a60a75369d0b4 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -9,6 +9,7 @@ from pandas._config.config import OptionError from pandas._libs.tslibs import ( + IncompatibleFrequency, OutOfBoundsDatetime, OutOfBoundsTimedelta, ) @@ -917,6 +918,7 @@ class InvalidComparison(Exception): "DuplicateLabelError", "EmptyDataError", "IncompatibilityWarning", + "IncompatibleFrequency", "IndexingError", "IntCastingNaNError", "InvalidColumnName", diff --git a/pandas/tests/indexes/period/test_indexing.py b/pandas/tests/indexes/period/test_indexing.py index 00e8262ddfa4c..75382cb735288 100644 --- a/pandas/tests/indexes/period/test_indexing.py +++ b/pandas/tests/indexes/period/test_indexing.py @@ -502,7 +502,7 @@ def test_get_indexer2(self): ) msg = "Input has different freq=None from PeriodArray\\(freq=h\\)" - with pytest.raises(ValueError, match=msg): + with pytest.raises(libperiod.IncompatibleFrequency, match=msg): idx.get_indexer(target, "nearest", tolerance="1 minute") tm.assert_numpy_array_equal( diff --git a/pandas/tests/indexes/period/test_join.py b/pandas/tests/indexes/period/test_join.py index 3e659c1a63266..9f733b358f772 100644 --- a/pandas/tests/indexes/period/test_join.py +++ b/pandas/tests/indexes/period/test_join.py @@ -1,7 +1,4 @@ import numpy as np -import pytest - -from pandas._libs.tslibs import IncompatibleFrequency from pandas import ( DataFrame, @@ -51,8 +48,9 @@ def test_join_does_not_recur(self): tm.assert_index_equal(res, expected) def test_join_mismatched_freq_raises(self): + # pre-GH#55782 this raises IncompatibleFrequency index = period_range("1/1/2000", "1/20/2000", freq="D") index3 = period_range("1/1/2000", "1/20/2000", freq="2D") - msg = r".*Input has different freq=2D from Period\(freq=D\)" - with pytest.raises(IncompatibleFrequency, match=msg): - index.join(index3) + result = index.join(index3) + expected = index.astype(object).join(index3.astype(object)) + tm.assert_index_equal(result, expected) diff --git a/pandas/tests/indexes/period/test_period.py b/pandas/tests/indexes/period/test_period.py index 77b8e76894647..d465225da7f24 100644 --- a/pandas/tests/indexes/period/test_period.py +++ b/pandas/tests/indexes/period/test_period.py @@ -1,6 +1,8 @@ import numpy as np import pytest +from pandas.errors import IncompatibleFrequency + from pandas import ( Index, NaT, @@ -198,7 +200,7 @@ def test_maybe_convert_timedelta(): offset = offsets.BusinessDay() msg = r"Input has different freq=B from PeriodIndex\(freq=D\)" - with pytest.raises(ValueError, match=msg): + with pytest.raises(IncompatibleFrequency, match=msg): pi._maybe_convert_timedelta(offset) diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index e7d284bd47e21..35a9742d653db 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -10,7 +10,6 @@ import pytest from pandas._libs import lib -from pandas._libs.tslibs import IncompatibleFrequency import pandas as pd from pandas import ( @@ -172,10 +171,6 @@ def test_add_series_with_period_index(self): result = ts + _permute(ts[::2]) tm.assert_series_equal(result, expected) - msg = "Input has different freq=D from Period\\(freq=Y-DEC\\)" - with pytest.raises(IncompatibleFrequency, match=msg): - ts + ts.asfreq("D", how="end") - @pytest.mark.parametrize( "target_add,input_value,expected_value", [ From 2d1b25b735c032510d596eb9e36835aa69877c52 Mon Sep 17 00:00:00 2001 From: Brock Date: Thu, 17 Jul 2025 08:02:00 -0700 Subject: [PATCH 2/3] docstring --- pandas/_libs/tslibs/period.pyx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 740f8d7b54f3b..df5c17745b8a4 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1626,6 +1626,10 @@ DIFFERENT_FREQ = ("Input has different freq={other_freq} " class IncompatibleFrequency(TypeError): + """ + Raised when trying to compare or operate between Periods with different + frequencies. + """ pass From 6e2d67d87fb656170d89e060e5f675cfd6e2e936 Mon Sep 17 00:00:00 2001 From: Brock Date: Thu, 17 Jul 2025 16:34:33 -0700 Subject: [PATCH 3/3] ignore docstring complaints --- ci/code_checks.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index a310b71d59da6..3a941deb2c68d 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -74,6 +74,7 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Series.dt PR01" `# Accessors are implemented as classes, but we do not document the Parameters section` \ -i "pandas.Period.freq GL08" \ -i "pandas.Period.ordinal GL08" \ + -i "pandas.errors.IncompatibleFrequency SA01,SS06,EX01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.resample.Resampler.quantile PR01,PR07" \