Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9a1c452
strict to true for info
LirongMa Sep 25, 2025
ee2bf48
Merge branch 'pandas-dev:main' into main
LirongMa Sep 26, 2025
70a8357
Merge branch 'pandas-dev:main' into main
LirongMa Sep 28, 2025
bae4e48
Merge branch 'pandas-dev:main' into main
LirongMa Oct 1, 2025
c324360
Merge branch 'pandas-dev:main' into main
LirongMa Oct 3, 2025
37fec55
add allow slice to is_hashable
LirongMa Oct 3, 2025
859f2f8
removed zip strict
LirongMa Oct 3, 2025
90cb69e
lint
LirongMa Oct 3, 2025
4544845
format
LirongMa Oct 3, 2025
0e4d675
format
LirongMa Oct 3, 2025
03c1b22
format
LirongMa Oct 3, 2025
8ded755
add allow_slice
LirongMa Oct 4, 2025
067b830
ruff
LirongMa Oct 4, 2025
8fcecee
fix ident
LirongMa Oct 4, 2025
871d621
doc
LirongMa Oct 4, 2025
d145d0b
test
LirongMa Oct 4, 2025
2b0ba79
test
LirongMa Oct 4, 2025
2a6e7a1
test
LirongMa Oct 4, 2025
7b83c16
test
LirongMa Oct 4, 2025
0a57d73
test
LirongMa Oct 4, 2025
c684c43
test
LirongMa Oct 4, 2025
06af37c
test
LirongMa Oct 4, 2025
fb3e85f
check allow_slice
LirongMa Oct 5, 2025
272a17b
removed example from code
LirongMa Oct 5, 2025
4440d70
var name
LirongMa Oct 5, 2025
fd30506
merge
LirongMa Oct 13, 2025
62793a4
add allow_slice=False
LirongMa Oct 13, 2025
2a3a0a8
remove comment
LirongMa Oct 13, 2025
1331063
merege main
LirongMa Oct 19, 2025
095f473
refactory
LirongMa Oct 19, 2025
0c97a82
change based on review comments
LirongMa Oct 31, 2025
5b2bc09
add comments
LirongMa Nov 5, 2025
cf46ad5
merge main
LirongMa Nov 5, 2025
46c78bd
pull main
LirongMa Nov 10, 2025
1cf2c6b
removed _contains_slice
LirongMa Nov 10, 2025
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
16 changes: 13 additions & 3 deletions pandas/core/dtypes/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def is_named_tuple(obj: object) -> bool:


@set_module("pandas.api.types")
def is_hashable(obj: object) -> TypeGuard[Hashable]:
def is_hashable(obj: object, allow_slice: bool = True) -> TypeGuard[Hashable]:
"""
Return True if hash(obj) will succeed, False otherwise.

Expand All @@ -399,13 +399,17 @@ def is_hashable(obj: object) -> TypeGuard[Hashable]:
----------
obj : object
The object to check for hashability. Any Python object can be passed here.
allow_slice : bool
If True, return True if the object is hashable (including slices).
If False, return True if the object is hashable and not a slice.

Returns
-------
bool
True if object can be hashed (i.e., does not raise TypeError when
passed to hash()), and False otherwise (e.g., if object is mutable
like a list or dictionary).
passed to hash()) and passes the slice check according to 'allow_slice'.
False otherwise (e.g., if object is mutable like a list or dictionary
or if allow_slice is False and object is a slice or contains a slice).

See Also
--------
Expand All @@ -431,6 +435,12 @@ def is_hashable(obj: object) -> TypeGuard[Hashable]:
# Reconsider this decision once this numpy bug is fixed:
# https://github.com/numpy/numpy/issues/5562

if allow_slice is False:
if isinstance(obj, tuple) and any(isinstance(v, slice) for v in obj):
return False
elif isinstance(obj, slice):
return False

try:
hash(obj)
except TypeError:
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -4093,7 +4093,7 @@ def __getitem__(self, key):
key = lib.item_from_zerodim(key)
key = com.apply_if_callable(key, self)

if is_hashable(key) and not is_iterator(key) and not isinstance(key, slice):
if is_hashable(key, allow_slice=False) and not is_iterator(key):
# is_iterator to exclude generator e.g. test_getitem_listlike
# As of Python 3.12, slice is hashable which breaks MultiIndex (GH#57500)

Expand Down
6 changes: 2 additions & 4 deletions pandas/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,8 +792,7 @@ def _get_setitem_indexer(self, key):
if (
isinstance(ax, MultiIndex)
and self.name != "iloc"
and is_hashable(key)
and not isinstance(key, slice)
and is_hashable(key, allow_slice=False)
):
with suppress(KeyError, InvalidIndexError):
# TypeError e.g. passed a bool
Expand Down Expand Up @@ -1146,8 +1145,7 @@ def _contains_slice(x: object) -> bool:
# This should never be reached, but let's be explicit about it
raise ValueError("Too many indices") # pragma: no cover
if all(
(is_hashable(x) and not _contains_slice(x)) or com.is_null_slice(x)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is the only usage of _contains_slice, so that can be removed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LirongMa can you address this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the _contains_slice function still exists a few lines up

for x in tup
is_hashable(x, allow_slice=False) or com.is_null_slice(x) for x in tup
):
# GH#10521 Series should reduce MultiIndex dimensions instead of
# DataFrame, IndexingError is not raised when slice(None,None,None)
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ def __getitem__(self, key):
if is_iterator(key):
key = list(key)

if is_hashable(key) and not isinstance(key, slice):
if is_hashable(key, allow_slice=False):
# Otherwise index.get_value will raise InvalidIndexError
try:
# For labels that don't resolve as scalars like tuples and frozensets
Expand Down
40 changes: 40 additions & 0 deletions pandas/tests/dtypes/test_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
missing as libmissing,
ops as libops,
)
from pandas.compat import PY312
from pandas.compat.numpy import np_version_gt2
from pandas.errors import Pandas4Warning

Expand Down Expand Up @@ -452,16 +453,55 @@ class UnhashableClass2:
def __hash__(self):
raise TypeError("Not hashable")

class HashableSlice:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment that we can get rid of that once we drop py311?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments added

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont see a comment

def __init__(self, start, stop, step=None):
self.slice = slice(start, stop, step)

def __eq__(self, other):
return isinstance(other, HashableSlice) and self.slice == other.slice

def __hash__(self):
return hash((self.slice.start, self.slice.stop, self.slice.step))

def __repr__(self):
return (
f"HashableSlice({self.slice.start}, {self.slice.stop}, "
f"{self.slice.step})"
)

hashable = (1, 3.14, np.float64(3.14), "a", (), (1,), HashableClass())
not_hashable = ([], UnhashableClass1())
abc_hashable_not_really_hashable = (([],), UnhashableClass2())
hashable_slice = HashableSlice(1, 2)
tuple_with_slice = (slice(1, 2), 3)

for i in hashable:
assert inference.is_hashable(i)
assert inference.is_hashable(i, allow_slice=True)
assert inference.is_hashable(i, allow_slice=False)
for i in not_hashable:
assert not inference.is_hashable(i)
assert not inference.is_hashable(i, allow_slice=True)
assert not inference.is_hashable(i, allow_slice=False)
for i in abc_hashable_not_really_hashable:
assert not inference.is_hashable(i)
assert not inference.is_hashable(i, allow_slice=True)
assert not inference.is_hashable(i, allow_slice=False)

assert inference.is_hashable(hashable_slice)
assert inference.is_hashable(hashable_slice, allow_slice=True)
assert inference.is_hashable(hashable_slice, allow_slice=False)

if PY312:
for obj in [slice(1, 2), tuple_with_slice]:
assert inference.is_hashable(obj)
assert inference.is_hashable(obj, allow_slice=True)
assert not inference.is_hashable(obj, allow_slice=False)
else:
for obj in [slice(1, 2), tuple_with_slice]:
assert not inference.is_hashable(obj)
assert not inference.is_hashable(obj, allow_slice=True)
assert not inference.is_hashable(obj, allow_slice=False)

# numpy.array is no longer collections.abc.Hashable as of
# https://github.com/numpy/numpy/pull/5326, just test
Expand Down
Loading