DEPR: deprecate returning a tuple from a callable in iloc indexing
State: closed

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.

- [x] closes #53533
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- ~[ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.~
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Labels:
name: Indexing
description: Related to indexing on series/frames, not to indexes themselves
name: Deprecate
description: Functionality to remove in pandas

Comments:
> I would think it more natural that the order of events in producing "normalized" indices would be:
>
> 1. apply callable if appropriate
> 2. apply tuple destructuring to obtain row and column keys
> 3. apply normal (non-callable/tuple) indexing rules to the row and column keys
>
> ...
>
> This PR deprecates (by explicitly warning in case (2)) returning a tuple from a callable. With the intention that it will in the future always raise `IndexError`.

Currently callables operate on  axis 0 only in loc/iloc. I'd be open to discussion on letting them operate using normal deconstruction, but that's maybe a bigger discussion, especially whether we should do the same for `.loc`. (For loc, there may be issues with that approach when an index is a `MultiIndex`).
[Aside: I did not write this quoted comment, please don't edit my comments to put words in my mouth]

> Currently callables operate on axis 0 only in loc/iloc. 

This is not true, `LocationIndexer` has this implementation in `__getitem__`:

```python
    def __getitem__(self, key):
        check_dict_or_set_indexers(key)
        if type(key) is tuple:
            key = tuple(list(x) if is_iterator(x) else x for x in key)
            key = tuple(com.apply_if_callable(x, self.obj) for x in key)
            if self._is_scalar_access(key):
                return self.obj._get_value(*key, takeable=self._takeable)
            return self._getitem_tuple(key)
```
So, if the key is a tuple, we destructure, and then apply the callable separately on the destructured axis-wise indexers if necessary.

Witness:
```python
In [2]: df = pd.DataFrame({"a": [1, 2, 3], "b": [1, 2, 3], "c": [2, 3, 4]})

In [3]: df.iloc[[0, 1], lambda df: [1, 2]]
Out[3]: 
   b  c
0  1  2
1  2  3

In [4]: df.loc[[1, 0], lambda df: ["a", "c"]]
Out[4]: 
   a  c
1  2  3
0  1  2

```
Ready for another look, thanks
Any thoughts on this change?
This pull request is stale because it has been open for thirty days with no activity. Please [update](https://pandas.pydata.org/pandas-docs/stable/development/contributing.html#updating-your-pull-request) and respond to this comment if you're still interested in working on this.
Thanks @wence- 
Thanks, and sorry for not doing a better job babysitting this.