Repository: pandas-dev/pandas
Pull request: DEPR: deprecate Index.__getitem__ with float key
See #34191 for motivation

Closes #34191

Problem statement:
DEPR: deprecate Index.__getitem__ with float key
I would propose to deprecate interpreting float numbers as integers in Index `__getitem__`.

Numpy does (no longer) allow this:

```
In [1]: a = np.array([1, 2, 3])   

In [3]: a[0.0]      
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-3-2ec1d2c6d8d4> in <module>
----> 1 a[0.0]

IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices
```

With a Series with `iloc`, we also don't allow this:

```
In [7]: pd.Series(a).iloc[0.0] 
...
TypeError: Cannot index by location index with a non-integer key
```

But for Index we do check if the float is integer-like and in that case convert to integer:

```
In [5]: pd.Index(a)[0.0]  
Out[5]: 1
```

However, this is not for all Index subclasses the case, eg:

```
In [9]: pd.DatetimeIndex(a)[0.0]                                                                                                                                                                                   
...
IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices
```

So I think we can simply deprecate this to align Index with numpy/Series/some Index subclasses.

Hints from issue discussion:
Can you specify which Index subclasses and methods would be affected?

In particular, would this address #33469, #31344, #7501
@jbrockmendel all the issue you link to are about `Series.__getitem__` with a float/integer index, this is about `Index.__getitem__` with a float/integer key. 

I had searched for related issues, but didn't directly find any, so opened this one.

This is the case for where `com.cast_scalar_indexer` is used in the `__setitem__` definition, which means the base class Index (and the numeric subclasses that inherit setitem) and MultiIndex, and *not* for any of the ExtensionIndex subclasses (since that redefines ``__setitem__`` without using  `com.cast_scalar_indexer`)
Thanks for clarifying.
There is a PR, if you want to see it in code ;)

Commit message: avoid warning in plotting code

Patch to review:
diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst
index 73892da2cbf71..56f8e6e58e9e8 100644
--- a/doc/source/whatsnew/v1.1.0.rst
+++ b/doc/source/whatsnew/v1.1.0.rst
@@ -586,6 +586,9 @@ Deprecations
 - :func:`pandas.api.types.is_categorical` is deprecated and will be removed in a future version; use `:func:pandas.api.types.is_categorical_dtype` instead (:issue:`33385`)
 - :meth:`Index.get_value` is deprecated and will be removed in a future version (:issue:`19728`)
 - :meth:`DateOffset.__call__` is deprecated and will be removed in a future version, use ``offset + other`` instead (:issue:`34171`)
+- Indexing an :class:`Index` object with a float key is deprecated, and will
+  raise an IndexError in the future. You can manually convert to an integer key
+  instead (:issue:`34191`).
 
 .. ---------------------------------------------------------------------------
 
diff --git a/pandas/core/common.py b/pandas/core/common.py
index 1ccca5193ab46..8bbb68df5ec24 100644
--- a/pandas/core/common.py
+++ b/pandas/core/common.py
@@ -9,6 +9,7 @@
 from functools import partial
 import inspect
 from typing import Any, Collection, Iterable, List, Union
+import warnings
 
 import numpy as np
 
@@ -144,13 +145,15 @@ def is_bool_indexer(key: Any) -> bool:
     return False
 
 
-def cast_scalar_indexer(val):
+def cast_scalar_indexer(val, warn_float=False):
     """
     To avoid numpy DeprecationWarnings, cast float to integer where valid.
 
     Parameters
     ----------
     val : scalar
+    warn_float : bool, default False
+        If True, raise deprecation warning for a float indexer.
 
     Returns
     -------
@@ -158,6 +161,13 @@ def cast_scalar_indexer(val):
     """
     # assumes lib.is_scalar(val)
     if lib.is_float(val) and val.is_integer():
+        if warn_float:
+            warnings.warn(
+                "Indexing with a float is deprecated, and will raise an IndexError "
+                "in pandas 2.0. You can manually convert to an integer key instead.",
+                FutureWarning,
+                stacklevel=3,
+            )
         return int(val)
     return val
 
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index b8a9827b5effd..1fcef08e96728 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -4039,7 +4039,7 @@ def __getitem__(self, key):
         promote = self._shallow_copy
 
         if is_scalar(key):
-            key = com.cast_scalar_indexer(key)
+            key = com.cast_scalar_indexer(key, warn_float=True)
             return getitem(key)
 
         if isinstance(key, slice):
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
index f1e1ebcaca1c4..5213137eb5c29 100644
--- a/pandas/core/indexes/multi.py
+++ b/pandas/core/indexes/multi.py
@@ -1896,7 +1896,7 @@ def __reduce__(self):
 
     def __getitem__(self, key):
         if is_scalar(key):
-            key = com.cast_scalar_indexer(key)
+            key = com.cast_scalar_indexer(key, warn_float=True)
 
             retval = []
             for lev, level_codes in zip(self.levels, self.codes):
diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py
index 19a75eb151782..4015a933b2fac 100644
--- a/pandas/plotting/_matplotlib/core.py
+++ b/pandas/plotting/_matplotlib/core.py
@@ -8,6 +8,7 @@
 from pandas.util._decorators import cache_readonly
 
 from pandas.core.dtypes.common import (
+    is_float,
     is_hashable,
     is_integer,
     is_iterator,
@@ -1188,6 +1189,8 @@ def _post_plot_logic(self, ax, data):
         from matplotlib.ticker import FixedLocator
 
         def get_label(i):
+            if is_float(i) and i.is_integer():
+                i = int(i)
             try:
                 return pprint_thing(data.index[i])
             except Exception:
diff --git a/pandas/tests/indexes/test_indexing.py b/pandas/tests/indexes/test_indexing.py
index a79bde9fd04e1..8910a3731cf8a 100644
--- a/pandas/tests/indexes/test_indexing.py
+++ b/pandas/tests/indexes/test_indexing.py
@@ -17,6 +17,7 @@
 import pytest
 
 from pandas import Float64Index, Index, Int64Index, UInt64Index
+import pandas._testing as tm
 
 
 class TestContains:
@@ -82,3 +83,16 @@ def test_contains_with_float_index(self):
         assert 1.1 in float_index
         assert 1.0 not in float_index
         assert 1 not in float_index
+
+
+@pytest.mark.parametrize(
+    "idx", [Index([1, 2, 3]), Index([0.1, 0.2, 0.3]), Index(["a", "b", "c"])]
+)
+def test_getitem_deprecated_float(idx):
+    # https://github.com/pandas-dev/pandas/issues/34191
+
+    with tm.assert_produces_warning(FutureWarning):
+        result = idx[1.0]
+
+    expected = idx[1]
+    assert result == expected


Code review comments:
diff_hunk: @@ -144,20 +145,29 @@ def is_bool_indexer(key: Any) -> bool:
     return False
 
 
-def cast_scalar_indexer(val):
+def cast_scalar_indexer(val, warn_float=False):
     """
     To avoid numpy DeprecationWarnings, cast float to integer where valid.
 
     Parameters
     ----------
     val : scalar
+    warn_float : bool, default False
+        If True, raise deprecation warning for a float indexer.
path: pandas/core/common.py
text: @user1:
"raise" -> "issue"