From 551d93360e6ab774bb10cdf23351d394744662a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E7=9F=A5=E6=81=92?= <1197843831@qq.com> Date: Fri, 24 Oct 2025 01:46:50 +0800 Subject: [PATCH 1/5] DOC: Update MultiIndex.take and MultiIndex.repeat docstring examples to use MultiIndex (#62809) --- pandas/core/indexes/multi.py | 29 +++++++++++++++++++ .../indexes/multi/test_datetime_indexing.py | 15 ++++++++++ 2 files changed, 44 insertions(+) create mode 100644 pandas/tests/indexes/multi/test_datetime_indexing.py diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 1cc1928136da1..c36207cfba7c2 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -3460,6 +3460,23 @@ def maybe_mi_droplevels(indexer, levels): return new_index + # type-check key against level(s), raise error if mismatch + if isinstance(key, tuple): + for i, k in enumerate(key): + if not self._is_key_type_compatible(k, i): + raise TypeError( + f"Type mismatch at index level {i}: " + f"expected {type(self.levels[i][0]).__name__}, " + f"got {type(k).__name__}" + ) + else: + if not self._is_key_type_compatible(key, level): + raise TypeError( + f"Type mismatch at index level {level}: " + f"expected {type(self.levels[level][0]).__name__}, " + f"got {type(key).__name__}" + ) + if isinstance(level, (tuple, list)): if len(key) != len(level): raise AssertionError( @@ -3591,6 +3608,18 @@ def maybe_mi_droplevels(indexer, levels): return indexer, result_index + def _is_key_type_compatible(self, key, level): + """ + Return True if the key type is compatible with the type of the level's values. + """ + if len(self.levels[level]) == 0: + return True # nothing to compare + level_type = type(self.levels[level][0]) + + # Same type → OK + if isinstance(key, level_type): + return True + def _get_level_indexer( self, key, level: int = 0, indexer: npt.NDArray[np.bool_] | None = None ): diff --git a/pandas/tests/indexes/multi/test_datetime_indexing.py b/pandas/tests/indexes/multi/test_datetime_indexing.py new file mode 100644 index 0000000000000..fdb4c0f9c12c7 --- /dev/null +++ b/pandas/tests/indexes/multi/test_datetime_indexing.py @@ -0,0 +1,15 @@ +import datetime + +import numpy as np +import pytest + +import pandas as pd + + +def test_multiindex_date_npdatetime_mismatch_raises(): + idx = pd.MultiIndex.from_product( + [[datetime.date(2023, 11, 1)], ["A"], ["C"]], names=["date", "t1", "t2"] + ) + df = pd.DataFrame({"vals": [1]}, index=idx) + with pytest.raises(TypeError, match="Type mismatch"): + df.loc[(np.datetime64("2023-11-01"), "A", "C")] From 8ab889e165aff1f9e8e3f3f375a3f8023ac29a51 Mon Sep 17 00:00:00 2001 From: Icaro Alves Date: Fri, 24 Oct 2025 13:17:24 +0000 Subject: [PATCH 2/5] DOC: added whatsnew entry for MultiIndex datetime/date fix (GH#55969) --- doc/source/whatsnew/v3.0.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 995e7676afbca..f64c9ffa9c7fa 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -1058,6 +1058,7 @@ Indexing - Bug in :meth:`DataFrame.loc.__getitem__` and :meth:`DataFrame.iloc.__getitem__` with a :class:`CategoricalDtype` column with integer categories raising when trying to index a row containing a ``NaN`` entry (:issue:`58954`) - Bug in :meth:`Index.__getitem__` incorrectly raising with a 0-dim ``np.ndarray`` key (:issue:`55601`) - Bug in :meth:`Index.get_indexer` not casting missing values correctly for new string datatype (:issue:`55833`) +- Bug in :meth:`MultiIndex._get_loc_level` raising ``TypeError`` when using a ``datetime.date`` key on a level containing incompatible objects (:issue:`55969`) - Bug in adding new rows with :meth:`DataFrame.loc.__setitem__` or :class:`Series.loc.__setitem__` which failed to retain dtype on the object's index in some cases (:issue:`41626`) - Bug in indexing on a :class:`DatetimeIndex` with a ``timestamp[pyarrow]`` dtype or on a :class:`TimedeltaIndex` with a ``duration[pyarrow]`` dtype (:issue:`62277`) From 78f3654ff6e1fbc500d07f7afb29c8e31c057dde Mon Sep 17 00:00:00 2001 From: Icaro Alves Date: Fri, 24 Oct 2025 13:59:07 +0000 Subject: [PATCH 3/5] BUG: refine MultiIndex key type check to allow compatible numpy scalars (GH#55969) --- pandas/core/indexes/multi.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index c36207cfba7c2..3fdbfca726e9f 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -8,6 +8,7 @@ Iterable, Sequence, ) +import datetime from functools import wraps from itertools import zip_longest from sys import getsizeof @@ -3619,6 +3620,19 @@ def _is_key_type_compatible(self, key, level): # Same type → OK if isinstance(key, level_type): return True + # Allow Python int for numpy integer types + if isinstance(level_type, np.integer) and isinstance(key, int): + return True + + # Allow Python float for numpy float types + if isinstance(level_type, np.floating) and isinstance(key, float): + return True + + # Allow subclasses of datetime.date for datetime levels + if isinstance(level_type, datetime.date) and isinstance(key, datetime.date): + return True + + return False def _get_level_indexer( self, key, level: int = 0, indexer: npt.NDArray[np.bool_] | None = None From c44d23619d0fd6ad5bcfd05077a145bfa934123b Mon Sep 17 00:00:00 2001 From: Icaro Alves Date: Fri, 24 Oct 2025 16:30:48 +0000 Subject: [PATCH 4/5] BUG: refactor _is_key_type_compatible and update test for MultiIndex datetime/date type checking (GH#55969) --- pandas/core/indexes/multi.py | 30 ++++++++++++------- .../indexes/multi/test_datetime_indexing.py | 21 +++++++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 3fdbfca726e9f..d192136a13da0 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -31,6 +31,7 @@ lib, ) from pandas._libs.hashtable import duplicated +from pandas._libs.tslibs.timestamps import Timestamp from pandas._typing import ( AnyAll, AnyArrayLike, @@ -3611,25 +3612,34 @@ def maybe_mi_droplevels(indexer, levels): def _is_key_type_compatible(self, key, level): """ - Return True if the key type is compatible with the type of the level's values. + Return True if the key is compatible with the type of the level's values. """ if len(self.levels[level]) == 0: return True # nothing to compare - level_type = type(self.levels[level][0]) - # Same type → OK - if isinstance(key, level_type): + level_type = self.levels[level][0] + + # Allow slices (used in partial indexing) + if isinstance(key, slice): return True - # Allow Python int for numpy integer types - if isinstance(level_type, np.integer) and isinstance(key, int): + + # datetime/date/Timestamp compatibility + datetime_types = (datetime.date, np.datetime64, Timestamp) + if isinstance(level_type, datetime_types) and isinstance( + key, datetime_types + (str,) + ): return True - # Allow Python float for numpy float types - if isinstance(level_type, np.floating) and isinstance(key, float): + # numeric compatibility + if np.issubdtype(type(level_type), np.integer) and isinstance(key, int): + return True + if np.issubdtype(type(level_type), np.floating) and isinstance( + key, (int, float) + ): return True - # Allow subclasses of datetime.date for datetime levels - if isinstance(level_type, datetime.date) and isinstance(key, datetime.date): + # string compatibility + if isinstance(level_type, str) and isinstance(key, str): return True return False diff --git a/pandas/tests/indexes/multi/test_datetime_indexing.py b/pandas/tests/indexes/multi/test_datetime_indexing.py index fdb4c0f9c12c7..77cc9f9fb14bc 100644 --- a/pandas/tests/indexes/multi/test_datetime_indexing.py +++ b/pandas/tests/indexes/multi/test_datetime_indexing.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import numpy as np import pytest @@ -7,9 +7,20 @@ def test_multiindex_date_npdatetime_mismatch_raises(): - idx = pd.MultiIndex.from_product( - [[datetime.date(2023, 11, 1)], ["A"], ["C"]], names=["date", "t1", "t2"] + dates = [dt.date(2023, 11, 1), dt.date(2023, 11, 1), dt.date(2023, 11, 2)] + t1 = ["A", "B", "C"] + t2 = ["C", "D", "E"] + vals = [10, 20, 30] + + df = pd.DataFrame( + data=np.array([dates, t1, t2, vals]).T, columns=["dates", "t1", "t2", "vals"] ) - df = pd.DataFrame({"vals": [1]}, index=idx) - with pytest.raises(TypeError, match="Type mismatch"): + df.set_index(["dates", "t1", "t2"], inplace=True) + + # Exact type match + result = df.loc[(dt.date(2023, 11, 1), "A", "C")] + assert result["vals"] == 10 + + # TypeError + with pytest.raises(KeyError): df.loc[(np.datetime64("2023-11-01"), "A", "C")] From c285c3f63f23ea17aedee486b95f63a0b8c2e77c Mon Sep 17 00:00:00 2001 From: Icaro Alves Date: Fri, 24 Oct 2025 17:09:04 +0000 Subject: [PATCH 5/5] BUG: refactor _is_key_type_compatible to contemplate CI tests (GH#55969) --- pandas/core/indexes/multi.py | 42 +++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index d192136a13da0..d5ec88ed82695 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -31,7 +31,6 @@ lib, ) from pandas._libs.hashtable import duplicated -from pandas._libs.tslibs.timestamps import Timestamp from pandas._typing import ( AnyAll, AnyArrayLike, @@ -3612,34 +3611,43 @@ def maybe_mi_droplevels(indexer, levels): def _is_key_type_compatible(self, key, level): """ - Return True if the key is compatible with the type of the level's values. + Return True if the key type is compatible with the type of the level's values. + + Compatible types: + - int ↔ np.integer + - float ↔ np.floating + - str ↔ np.str_ + - datetime.date ↔ datetime.datetime + - slices (for partial indexing) """ if len(self.levels[level]) == 0: return True # nothing to compare - level_type = self.levels[level][0] + level_val = self.levels[level][0] + level_type = type(level_val) - # Allow slices (used in partial indexing) - if isinstance(key, slice): + # Same type + if isinstance(key, level_type): return True - # datetime/date/Timestamp compatibility - datetime_types = (datetime.date, np.datetime64, Timestamp) - if isinstance(level_type, datetime_types) and isinstance( - key, datetime_types + (str,) - ): + # NumPy integer / float / string compatibility + if isinstance(level_val, np.integer) and isinstance(key, int): + return True + if isinstance(level_val, np.floating) and isinstance(key, float): + return True + if isinstance(level_val, np.str_) and isinstance(key, str): return True - # numeric compatibility - if np.issubdtype(type(level_type), np.integer) and isinstance(key, int): + # Allow subclasses of datetime.date for datetime levels + if isinstance(level_val, datetime.date) and isinstance(key, datetime.date): return True - if np.issubdtype(type(level_type), np.floating) and isinstance( - key, (int, float) - ): + + # Allow slices (used internally for partial selection) + if isinstance(key, slice): return True - # string compatibility - if isinstance(level_type, str) and isinstance(key, str): + # Allow any NumPy generic types for flexibility + if isinstance(key, np.generic): return True return False