Skip to content

Commit 0e2277b

Browse files
wence-mroeschke
andauthored
DEPR: deprecate returning a tuple from a callable in iloc indexing (#53769)
* DEPR: deprecate returning a tuple from a callable in iloc indexing The current semantics are that tuple-destructuring of the key is performed before unwinding any callables. As such, if a callable returns a tuple for iloc, it will be handled incorrectly. To avoid this, explicitly deprecate support for this behaviour. Closes #53533. * Update documentation and add test * Refactor check into method * Link to docs in whatsnew entry * Move deprecation warning so docstring checks don't complain * Prelim changes * Update pandas/core/indexing.py * Adjust test --------- Co-authored-by: Matthew Roeschke <[email protected]>
1 parent b8fc8cd commit 0e2277b

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

doc/source/user_guide/indexing.rst

+18
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ of multi-axis indexing.
6262
* A boolean array (any ``NA`` values will be treated as ``False``).
6363
* A ``callable`` function with one argument (the calling Series or DataFrame) and
6464
that returns valid output for indexing (one of the above).
65+
* A tuple of row (and column) indices whose elements are one of the
66+
above inputs.
6567

6668
See more at :ref:`Selection by Label <indexing.label>`.
6769

@@ -78,13 +80,21 @@ of multi-axis indexing.
7880
* A boolean array (any ``NA`` values will be treated as ``False``).
7981
* A ``callable`` function with one argument (the calling Series or DataFrame) and
8082
that returns valid output for indexing (one of the above).
83+
* A tuple of row (and column) indices whose elements are one of the
84+
above inputs.
8185

8286
See more at :ref:`Selection by Position <indexing.integer>`,
8387
:ref:`Advanced Indexing <advanced>` and :ref:`Advanced
8488
Hierarchical <advanced.advanced_hierarchical>`.
8589

8690
* ``.loc``, ``.iloc``, and also ``[]`` indexing can accept a ``callable`` as indexer. See more at :ref:`Selection By Callable <indexing.callable>`.
8791

92+
.. note::
93+
94+
Destructuring tuple keys into row (and column) indexes occurs
95+
*before* callables are applied, so you cannot return a tuple from
96+
a callable to index both rows and columns.
97+
8898
Getting values from an object with multi-axes selection uses the following
8999
notation (using ``.loc`` as an example, but the following applies to ``.iloc`` as
90100
well). Any of the axes accessors may be the null slice ``:``. Axes left out of
@@ -450,6 +460,8 @@ The ``.iloc`` attribute is the primary access method. The following are valid in
450460
* A slice object with ints ``1:7``.
451461
* A boolean array.
452462
* A ``callable``, see :ref:`Selection By Callable <indexing.callable>`.
463+
* A tuple of row (and column) indexes, whose elements are one of the
464+
above types.
453465

454466
.. ipython:: python
455467
@@ -553,6 +565,12 @@ Selection by callable
553565
``.loc``, ``.iloc``, and also ``[]`` indexing can accept a ``callable`` as indexer.
554566
The ``callable`` must be a function with one argument (the calling Series or DataFrame) that returns valid output for indexing.
555567

568+
.. note::
569+
570+
For ``.iloc`` indexing, returning a tuple from the callable is
571+
not supported, since tuple destructuring for row and column indexes
572+
occurs *before* applying callables.
573+
556574
.. ipython:: python
557575
558576
df1 = pd.DataFrame(np.random.randn(6, 4),

pandas/core/indexing.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import sys
55
from typing import (
66
TYPE_CHECKING,
7+
Any,
8+
TypeVar,
79
cast,
810
final,
911
)
@@ -25,6 +27,7 @@
2527
_chained_assignment_msg,
2628
)
2729
from pandas.util._decorators import doc
30+
from pandas.util._exceptions import find_stack_level
2831

2932
from pandas.core.dtypes.cast import (
3033
can_hold_element,
@@ -90,6 +93,7 @@
9093
Series,
9194
)
9295

96+
T = TypeVar("T")
9397
# "null slice"
9498
_NS = slice(None, None)
9599
_one_ellipsis_message = "indexer may only contain one '...' entry"
@@ -153,6 +157,10 @@ def iloc(self) -> _iLocIndexer:
153157
"""
154158
Purely integer-location based indexing for selection by position.
155159
160+
.. deprecated:: 2.2.0
161+
162+
Returning a tuple from a callable is deprecated.
163+
156164
``.iloc[]`` is primarily integer position based (from ``0`` to
157165
``length-1`` of the axis), but may also be used with a boolean
158166
array.
@@ -166,7 +174,8 @@ def iloc(self) -> _iLocIndexer:
166174
- A ``callable`` function with one argument (the calling Series or
167175
DataFrame) and that returns valid output for indexing (one of the above).
168176
This is useful in method chains, when you don't have a reference to the
169-
calling object, but would like to base your selection on some value.
177+
calling object, but would like to base your selection on
178+
some value.
170179
- A tuple of row and column indexes. The tuple elements consist of one of the
171180
above inputs, e.g. ``(0, 1)``.
172181
@@ -878,7 +887,8 @@ def __setitem__(self, key, value) -> None:
878887
key = tuple(list(x) if is_iterator(x) else x for x in key)
879888
key = tuple(com.apply_if_callable(x, self.obj) for x in key)
880889
else:
881-
key = com.apply_if_callable(key, self.obj)
890+
maybe_callable = com.apply_if_callable(key, self.obj)
891+
key = self._check_deprecated_callable_usage(key, maybe_callable)
882892
indexer = self._get_setitem_indexer(key)
883893
self._has_valid_setitem_indexer(key)
884894

@@ -1137,6 +1147,17 @@ def _contains_slice(x: object) -> bool:
11371147
def _convert_to_indexer(self, key, axis: AxisInt):
11381148
raise AbstractMethodError(self)
11391149

1150+
def _check_deprecated_callable_usage(self, key: Any, maybe_callable: T) -> T:
1151+
# GH53533
1152+
if self.name == "iloc" and callable(key) and isinstance(maybe_callable, tuple):
1153+
warnings.warn(
1154+
"Returning a tuple from a callable with iloc "
1155+
"is deprecated and will be removed in a future version",
1156+
FutureWarning,
1157+
stacklevel=find_stack_level(),
1158+
)
1159+
return maybe_callable
1160+
11401161
@final
11411162
def __getitem__(self, key):
11421163
check_dict_or_set_indexers(key)
@@ -1151,6 +1172,7 @@ def __getitem__(self, key):
11511172
axis = self.axis or 0
11521173

11531174
maybe_callable = com.apply_if_callable(key, self.obj)
1175+
maybe_callable = self._check_deprecated_callable_usage(key, maybe_callable)
11541176
return self._getitem_axis(maybe_callable, axis=axis)
11551177

11561178
def _is_scalar_access(self, key: tuple):

pandas/tests/frame/indexing/test_indexing.py

+9
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,15 @@ def test_single_element_ix_dont_upcast(self, float_frame):
10171017
result = df.loc[[0], "b"]
10181018
tm.assert_series_equal(result, expected)
10191019

1020+
def test_iloc_callable_tuple_return_value(self):
1021+
# GH53769
1022+
df = DataFrame(np.arange(40).reshape(10, 4), index=range(0, 20, 2))
1023+
msg = "callable with iloc"
1024+
with tm.assert_produces_warning(FutureWarning, match=msg):
1025+
df.iloc[lambda _: (0,)]
1026+
with tm.assert_produces_warning(FutureWarning, match=msg):
1027+
df.iloc[lambda _: (0,)] = 1
1028+
10201029
def test_iloc_row(self):
10211030
df = DataFrame(
10221031
np.random.default_rng(2).standard_normal((10, 4)), index=range(0, 20, 2)

0 commit comments

Comments
 (0)