Skip to content

Commit 06e416d

Browse files
jbrockmendeljreback
authored andcommitted
BUG: raise on non-hashable in __contains__ (#30902)
1 parent 264363e commit 06e416d

File tree

10 files changed

+37
-19
lines changed

10 files changed

+37
-19
lines changed

pandas/_libs/index.pyx

+9-4
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ cdef class IndexEngine:
7272
self.over_size_threshold = n >= _SIZE_CUTOFF
7373
self.clear_mapping()
7474

75-
def __contains__(self, object val):
75+
def __contains__(self, val: object) -> bool:
76+
# We assume before we get here:
77+
# - val is hashable
7678
self._ensure_mapping_populated()
77-
hash(val)
7879
return val in self.mapping
7980

8081
cpdef get_value(self, ndarray arr, object key, object tz=None):
@@ -415,7 +416,9 @@ cdef class DatetimeEngine(Int64Engine):
415416
raise TypeError(scalar)
416417
return scalar.value
417418

418-
def __contains__(self, object val):
419+
def __contains__(self, val: object) -> bool:
420+
# We assume before we get here:
421+
# - val is hashable
419422
cdef:
420423
int64_t loc, conv
421424

@@ -712,7 +715,9 @@ cdef class BaseMultiIndexCodesEngine:
712715

713716
return indexer
714717

715-
def __contains__(self, object val):
718+
def __contains__(self, val: object) -> bool:
719+
# We assume before we get here:
720+
# - val is hashable
716721
# Default __contains__ looks in the underlying mapping, which in this
717722
# case only contains integer representations.
718723
try:

pandas/core/indexes/base.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22
import operator
33
from textwrap import dedent
4-
from typing import Dict, FrozenSet, Hashable, Optional, Union
4+
from typing import Any, Dict, FrozenSet, Hashable, Optional, Union
55
import warnings
66

77
import numpy as np
@@ -4144,7 +4144,7 @@ def is_type_compatible(self, kind) -> bool:
41444144
"""
41454145

41464146
@Appender(_index_shared_docs["contains"] % _index_doc_kwargs)
4147-
def __contains__(self, key) -> bool:
4147+
def __contains__(self, key: Any) -> bool:
41484148
hash(key)
41494149
try:
41504150
return key in self._engine

pandas/core/indexes/category.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,12 @@ def _wrap_setop_result(self, other, result):
385385
return self._shallow_copy(result, name=name)
386386

387387
@Appender(_index_shared_docs["contains"] % _index_doc_kwargs)
388-
def __contains__(self, key) -> bool:
388+
def __contains__(self, key: Any) -> bool:
389389
# if key is a NaN, check if any NaN is in self.
390390
if is_scalar(key) and isna(key):
391391
return self.hasnans
392392

393+
hash(key)
393394
return contains(self, key, container=self._engine)
394395

395396
def __array__(self, dtype=None) -> np.ndarray:

pandas/core/indexes/datetimelike.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Base and utility classes for tseries type pandas objects.
33
"""
44
import operator
5-
from typing import List, Optional, Set
5+
from typing import Any, List, Optional, Set
66

77
import numpy as np
88

@@ -154,7 +154,8 @@ def equals(self, other) -> bool:
154154
return np.array_equal(self.asi8, other.asi8)
155155

156156
@Appender(_index_shared_docs["contains"] % _index_doc_kwargs)
157-
def __contains__(self, key):
157+
def __contains__(self, key: Any) -> bool:
158+
hash(key)
158159
try:
159160
res = self.get_loc(key)
160161
except (KeyError, TypeError, ValueError):

pandas/core/indexes/interval.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ def _engine(self):
374374
right = self._maybe_convert_i8(self.right)
375375
return IntervalTree(left, right, closed=self.closed)
376376

377-
def __contains__(self, key) -> bool:
377+
def __contains__(self, key: Any) -> bool:
378378
"""
379379
return a boolean if this key is IN the index
380380
We *only* accept an Interval
@@ -387,6 +387,7 @@ def __contains__(self, key) -> bool:
387387
-------
388388
bool
389389
"""
390+
hash(key)
390391
if not isinstance(key, Interval):
391392
return False
392393

pandas/core/indexes/multi.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import datetime
22
from sys import getsizeof
3-
from typing import Hashable, List, Optional, Sequence, Union
3+
from typing import Any, Hashable, List, Optional, Sequence, Union
44
import warnings
55

66
import numpy as np
@@ -973,7 +973,7 @@ def _shallow_copy_with_infer(self, values, **kwargs):
973973
return self._shallow_copy(values, **kwargs)
974974

975975
@Appender(_index_shared_docs["contains"] % _index_doc_kwargs)
976-
def __contains__(self, key) -> bool:
976+
def __contains__(self, key: Any) -> bool:
977977
hash(key)
978978
try:
979979
self.get_loc(key)

pandas/core/indexes/numeric.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING
1+
from typing import TYPE_CHECKING, Any
22

33
import numpy as np
44

@@ -461,7 +461,8 @@ def equals(self, other) -> bool:
461461
except (TypeError, ValueError):
462462
return False
463463

464-
def __contains__(self, other) -> bool:
464+
def __contains__(self, other: Any) -> bool:
465+
hash(other)
465466
if super().__contains__(other):
466467
return True
467468

pandas/core/indexes/period.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime, timedelta
2+
from typing import Any
23
import weakref
34

45
import numpy as np
@@ -358,18 +359,18 @@ def _engine(self):
358359
return self._engine_type(period, len(self))
359360

360361
@Appender(_index_shared_docs["contains"])
361-
def __contains__(self, key) -> bool:
362+
def __contains__(self, key: Any) -> bool:
362363
if isinstance(key, Period):
363364
if key.freq != self.freq:
364365
return False
365366
else:
366367
return key.ordinal in self._engine
367368
else:
369+
hash(key)
368370
try:
369371
self.get_loc(key)
370372
return True
371-
except (TypeError, KeyError):
372-
# TypeError can be reached if we pass a tuple that is not hashable
373+
except KeyError:
373374
return False
374375

375376
@cache_readonly

pandas/core/indexes/range.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import timedelta
22
import operator
33
from sys import getsizeof
4-
from typing import Optional, Union
4+
from typing import Any, Optional
55
import warnings
66

77
import numpy as np
@@ -332,7 +332,7 @@ def is_monotonic_decreasing(self) -> bool:
332332
def has_duplicates(self) -> bool:
333333
return False
334334

335-
def __contains__(self, key: Union[int, np.integer]) -> bool:
335+
def __contains__(self, key: Any) -> bool:
336336
hash(key)
337337
try:
338338
key = ensure_python_int(key)

pandas/tests/indexes/common.py

+8
Original file line numberDiff line numberDiff line change
@@ -883,3 +883,11 @@ def test_getitem_2d_deprecated(self):
883883
res = idx[:, None]
884884

885885
assert isinstance(res, np.ndarray), type(res)
886+
887+
def test_contains_requires_hashable_raises(self):
888+
idx = self.create_index()
889+
with pytest.raises(TypeError, match="unhashable type"):
890+
[] in idx
891+
892+
with pytest.raises(TypeError):
893+
{} in idx._engine

0 commit comments

Comments
 (0)