Skip to content

Commit dd54dc9

Browse files
committed
make frame and series non-hashable
1 parent 4ec6925 commit dd54dc9

File tree

9 files changed

+32
-31
lines changed

9 files changed

+32
-31
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ Other API changes
612612
- Partially initialized :class:`CategoricalDtype` (i.e. those with ``categories=None`` objects will no longer compare as equal to fully initialized dtype objects.
613613
- Accessing ``_constructor_expanddim`` on a :class:`DataFrame` and ``_constructor_sliced`` on a :class:`Series` now raise an ``AttributeError``. Previously a ``NotImplementedError`` was raised (:issue:`38782`)
614614
- Added new ``engine`` and ``**engine_kwargs`` parameters to :meth:`DataFrame.to_sql` to support other future "SQL engines". Currently we still only use ``SQLAlchemy`` under the hood, but more engines are planned to be supported such as ``turbodbc`` (:issue:`36893`)
615+
- Calling ``hash`` on pandas objects will now raise ``TypeError`` with the built-in error message (e.g. ``unhashable type: 'Series'``). Previously it would raise a custom message such as ``"'Series' objects are mutable, thus they cannot be hashed"`` (:issue:`40013`)
615616

616617
Build
617618
=====

pandas/core/arrays/base.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1302,8 +1302,10 @@ def _reduce(self, name: str, *, skipna: bool = True, **kwargs):
13021302
"""
13031303
raise TypeError(f"cannot perform {name} with type {self.dtype}")
13041304

1305-
def __hash__(self) -> int:
1306-
raise TypeError(f"unhashable type: {repr(type(self).__name__)}")
1305+
# https://github.com/python/typeshed/issues/2148#issuecomment-520783318
1306+
# Incompatible types in assignment (expression has type "None", base class
1307+
# "object" defined the type as "Callable[[object], int]")
1308+
__hash__: None # type: ignore[assignment]
13071309

13081310
# ------------------------------------------------------------------------
13091311
# Non-Optimized Default Methods

pandas/core/frame.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -6152,26 +6152,26 @@ def f(vals) -> tuple[np.ndarray, int]:
61526152
return labels.astype("i8", copy=False), len(shape)
61536153

61546154
if subset is None:
6155-
subset = self.columns
6155+
subset_iterable: Iterable = self.columns
61566156
elif (
61576157
not np.iterable(subset)
61586158
or isinstance(subset, str)
61596159
or isinstance(subset, tuple)
61606160
and subset in self.columns
61616161
):
6162-
subset = (subset,)
6163-
6164-
# needed for mypy since can't narrow types using np.iterable
6165-
subset = cast(Iterable, subset)
6162+
subset_iterable = (subset,)
6163+
else:
6164+
# needed for mypy since can't narrow types using np.iterable
6165+
subset_iterable = cast(Iterable, subset)
61666166

61676167
# Verify all columns in subset exist in the queried dataframe
61686168
# Otherwise, raise a KeyError, same as if you try to __getitem__ with a
61696169
# key that doesn't exist.
6170-
diff = Index(subset).difference(self.columns)
6170+
diff = Index(subset_iterable).difference(self.columns)
61716171
if not diff.empty:
61726172
raise KeyError(diff)
61736173

6174-
vals = (col.values for name, col in self.items() if name in subset)
6174+
vals = (col.values for name, col in self.items() if name in subset_iterable)
61756175
labels, shape = map(list, zip(*map(f, vals)))
61766176

61776177
ids = get_group_index(

pandas/core/generic.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -1872,11 +1872,10 @@ def _drop_labels_or_levels(self, keys, axis: int = 0):
18721872
# ----------------------------------------------------------------------
18731873
# Iteration
18741874

1875-
def __hash__(self) -> int:
1876-
raise TypeError(
1877-
f"{repr(type(self).__name__)} objects are mutable, "
1878-
f"thus they cannot be hashed"
1879-
)
1875+
# https://github.com/python/typeshed/issues/2148#issuecomment-520783318
1876+
# Incompatible types in assignment (expression has type "None", base class
1877+
# "object" defined the type as "Callable[[object], int]")
1878+
__hash__: None # type: ignore[assignment]
18801879

18811880
def __iter__(self):
18821881
"""

pandas/core/indexes/base.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -4558,9 +4558,10 @@ def __contains__(self, key: Any) -> bool:
45584558
except (OverflowError, TypeError, ValueError):
45594559
return False
45604560

4561-
@final
4562-
def __hash__(self):
4563-
raise TypeError(f"unhashable type: {repr(type(self).__name__)}")
4561+
# https://github.com/python/typeshed/issues/2148#issuecomment-520783318
4562+
# Incompatible types in assignment (expression has type "None", base class
4563+
# "object" defined the type as "Callable[[object], int]")
4564+
__hash__: None # type: ignore[assignment]
45644565

45654566
@final
45664567
def __setitem__(self, key, value):

pandas/core/reshape/pivot.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ def pivot(
482482
if columns is None:
483483
raise TypeError("pivot() missing 1 required argument: 'columns'")
484484

485-
columns = com.convert_to_list_like(columns)
485+
columns_listlike = com.convert_to_list_like(columns)
486486

487487
if values is None:
488488
if index is not None:
@@ -494,28 +494,27 @@ def pivot(
494494
# error: Unsupported operand types for + ("List[Any]" and "ExtensionArray")
495495
# error: Unsupported left operand type for + ("ExtensionArray")
496496
indexed = data.set_index(
497-
cols + columns, append=append # type: ignore[operator]
497+
cols + columns_listlike, append=append # type: ignore[operator]
498498
)
499499
else:
500500
if index is None:
501-
index = [Series(data.index, name=data.index.name)]
501+
index_list = [Series(data.index, name=data.index.name)]
502502
else:
503-
index = com.convert_to_list_like(index)
504-
index = [data[idx] for idx in index]
503+
index_list = [data[idx] for idx in com.convert_to_list_like(index)]
505504

506-
data_columns = [data[col] for col in columns]
507-
index.extend(data_columns)
508-
index = MultiIndex.from_arrays(index)
505+
data_columns = [data[col] for col in columns_listlike]
506+
index_list.extend(data_columns)
507+
multiindex = MultiIndex.from_arrays(index_list)
509508

510509
if is_list_like(values) and not isinstance(values, tuple):
511510
# Exclude tuple because it is seen as a single column name
512511
values = cast(Sequence[Hashable], values)
513512
indexed = data._constructor(
514-
data[values]._values, index=index, columns=values
513+
data[values]._values, index=multiindex, columns=values
515514
)
516515
else:
517-
indexed = data._constructor_sliced(data[values]._values, index=index)
518-
return indexed.unstack(columns)
516+
indexed = data._constructor_sliced(data[values]._values, index=multiindex)
517+
return indexed.unstack(columns_listlike)
519518

520519

521520
def crosstab(

pandas/core/series.py

-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,6 @@ class Series(base.IndexOpsMixin, generic.NDFrame):
272272
hasnans = property( # type: ignore[assignment]
273273
base.IndexOpsMixin.hasnans.func, doc=base.IndexOpsMixin.hasnans.__doc__
274274
)
275-
__hash__ = generic.NDFrame.__hash__
276275
_mgr: SingleManager
277276
div: Callable[[Series, Any], Series]
278277
rdiv: Callable[[Series, Any], Series]

pandas/tests/frame/test_api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def test_not_hashable(self):
9191
empty_frame = DataFrame()
9292

9393
df = DataFrame([1])
94-
msg = "'DataFrame' objects are mutable, thus they cannot be hashed"
94+
msg = "unhashable type: 'DataFrame'"
9595
with pytest.raises(TypeError, match=msg):
9696
hash(df)
9797
with pytest.raises(TypeError, match=msg):

pandas/tests/series/test_api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def test_index_tab_completion(self, index):
101101
def test_not_hashable(self):
102102
s_empty = Series(dtype=object)
103103
s = Series([1])
104-
msg = "'Series' objects are mutable, thus they cannot be hashed"
104+
msg = "unhashable type: 'Series'"
105105
with pytest.raises(TypeError, match=msg):
106106
hash(s_empty)
107107
with pytest.raises(TypeError, match=msg):

0 commit comments

Comments
 (0)