diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 34a67836f9675..7449c62a5ad31 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -56,6 +56,8 @@ Other API changes - :meth:`Series.describe` will now show distribution percentiles for ``datetime`` dtypes, statistics ``first`` and ``last`` will now be ``min`` and ``max`` to match with numeric dtypes in :meth:`DataFrame.describe` (:issue:`30164`) - :meth:`Groupby.groups` now returns an abbreviated representation when called on large dataframes (:issue:`1135`) +- ``loc`` lookups with an object-dtype :class:`Index` and an integer key will now raise ``KeyError`` instead of ``TypeError`` when key is missing (:issue:`31905`) +- Backwards incompatible API changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -160,6 +162,8 @@ Indexing - Bug in :meth:`DatetimeIndex.get_loc` raising ``KeyError`` with converted-integer key instead of the user-passed key (:issue:`31425`) - Bug in :meth:`Series.xs` incorrectly returning ``Timestamp`` instead of ``datetime64`` in some object-dtype cases (:issue:`31630`) - Bug in :meth:`DataFrame.iat` incorrectly returning ``Timestamp`` instead of ``datetime`` in some object-dtype cases (:issue:`32809`) +- Bug in :meth:`Series.loc` and :meth:`DataFrame.loc` when indexing with an integer key on a object-dtype :class:`Index` that is not all-integers (:issue:`31905`) +- Missing ^^^^^^^ diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 14ee21ea5614c..32ebecf1f223d 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3128,7 +3128,7 @@ def _convert_scalar_indexer(self, key, kind: str_t): self._invalid_indexer("label", key) elif kind == "loc" and is_integer(key): - if not self.holds_integer(): + if not (is_integer_dtype(self.dtype) or is_object_dtype(self.dtype)): self._invalid_indexer("label", key) return key diff --git a/pandas/core/series.py b/pandas/core/series.py index 9c0ff9780da3e..eac92695ed075 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -969,9 +969,11 @@ def _get_value(self, label, takeable: bool = False): if takeable: return self._values[label] + # Similar to Index.get_value, but we do not fall back to positional + loc = self.index.get_loc(label) # We assume that _convert_scalar_indexer has already been called, # with kind="loc", if necessary, by the time we get here - return self.index.get_value(self, label) + return self.index._get_values_for_loc(self, loc, label) def __setitem__(self, key, value): key = com.apply_if_callable(key, self) diff --git a/pandas/tests/indexing/test_categorical.py b/pandas/tests/indexing/test_categorical.py index da935b1c911d0..8a8ac584c16c2 100644 --- a/pandas/tests/indexing/test_categorical.py +++ b/pandas/tests/indexing/test_categorical.py @@ -82,11 +82,7 @@ def test_loc_scalar(self): with pytest.raises(TypeError, match=msg): df.loc["d", "C"] = 10 - msg = ( - "cannot do label indexing on CategoricalIndex with these " - r"indexers \[1\] of type int" - ) - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^1$"): df.loc[1] def test_getitem_scalar(self): diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 71d85ed8bda9b..276d11a67ad18 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -16,7 +16,7 @@ class TestLoc(Base): def test_loc_getitem_int(self): # int label - self.check_result("loc", 2, typs=["labels"], fails=TypeError) + self.check_result("loc", 2, typs=["labels"], fails=KeyError) def test_loc_getitem_label(self): @@ -34,7 +34,7 @@ def test_loc_getitem_label_out_of_range(self): self.check_result( "loc", 20, typs=["ints", "uints", "mixed"], fails=KeyError, ) - self.check_result("loc", 20, typs=["labels"], fails=TypeError) + self.check_result("loc", 20, typs=["labels"], fails=KeyError) self.check_result("loc", 20, typs=["ts"], axes=0, fails=TypeError) self.check_result("loc", 20, typs=["floats"], axes=0, fails=KeyError) @@ -967,3 +967,11 @@ def test_loc_set_dataframe_multiindex(): result = expected.copy() result.loc[0, [(0, 1)]] = result.loc[0, [(0, 1)]] tm.assert_frame_equal(result, expected) + + +def test_loc_mixed_int_float(): + # GH#19456 + ser = pd.Series(range(2), pd.Index([1, 2.0], dtype=object)) + + result = ser.loc[1] + assert result == 0 diff --git a/pandas/tests/indexing/test_scalar.py b/pandas/tests/indexing/test_scalar.py index c4750778e2eb8..25939e63c256b 100644 --- a/pandas/tests/indexing/test_scalar.py +++ b/pandas/tests/indexing/test_scalar.py @@ -138,16 +138,12 @@ def test_series_at_raises_type_error(self): result = ser.loc["a"] assert result == 1 - msg = ( - "cannot do label indexing on Index " - r"with these indexers \[0\] of type int" - ) - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^0$"): ser.at[0] - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^0$"): ser.loc[0] - def test_frame_raises_type_error(self): + def test_frame_raises_key_error(self): # GH#31724 .at should match .loc df = DataFrame({"A": [1, 2, 3]}, index=list("abc")) result = df.at["a", "A"] @@ -155,13 +151,9 @@ def test_frame_raises_type_error(self): result = df.loc["a", "A"] assert result == 1 - msg = ( - "cannot do label indexing on Index " - r"with these indexers \[0\] of type int" - ) - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^0$"): df.at["a", 0] - with pytest.raises(TypeError, match=msg): + with pytest.raises(KeyError, match="^0$"): df.loc["a", 0] def test_series_at_raises_key_error(self):