Repository: pandas-dev/pandas
File: pandas/core/groupby/generic.py (Python)

Code before:
        Sequence,
    )

    from pandas._typing import (
        ArrayLike,
        BlockManager,
        CorrelationMethod,
        IndexLabel,
        Manager,
        SingleBlockManager,
        TakeIndexer,
    )

    from pandas import Categorical
    from pandas.core.generic import NDFrame

# TODO(typing) the return value on this callable should be any *scalar*.
AggScalar: TypeAlias = str | Callable[..., Any]
# TODO: validate types on ScalarResult and move to _typing
# Blocked from using by https://github.com/python/mypy/issues/1484
# See note at _mangle_lambda_list
ScalarResult = TypeVar("ScalarResult")


@set_module("pandas")
class NamedAgg(tuple):
    """
    Helper for column specific aggregation with control over output column names.

    Subclass of tuple.

    Parameters
    ----------
    column : Hashable
        Column label in the DataFrame to apply aggfunc.
    aggfunc : function or str
        Function to apply to the provided column. If string, the name of a built-in
        pandas function.
    *args, **kwargs :
        Optional positional and keyword arguments passed to ``aggfunc``.

    See Also
    --------
    DataFrame.groupby : Group DataFrame using a mapper or by a Series of columns.

    Examples
    --------
    >>> df = pd.DataFrame({"key": [1, 1, 2], "a": [-1, 0, 1], "b": [10, 11, 12]})
    >>> agg_a = pd.NamedAgg(column="a", aggfunc="min")
    >>> agg_b = pd.NamedAgg(column="b", aggfunc=lambda x: x.mean())
    >>> df.groupby("key").agg(result_a=agg_a, result_b=agg_b)

Review comment (suggestion):
This seems too breaking. Previously, users could access `NamedAgg.column` after creation, but not if we inherit from tuple. Can we use a dataclass here instead:

```python
@dataclasses.dataclass
class NamedAgg:
    column: Hashable
    aggfunc: AggScalar
    args: tuple = ()
    kwargs: dict = dataclasses.field(default_factory=dict)

    def __init__(self, column: Hashable, aggfunc: AggScalar, *args, **kwargs) -> None:
        self.column = column
        self.aggfunc = aggfunc
        self.args = args
        self.kwargs = kwargs

    def __getitem__(self, key: int):
        if key == 0:
            return self.column
        elif key == 1:
            return self.aggfunc
        elif key == 2:
            return self.args
        elif key == 3:
            return self.kwargs
        raise IndexError("index out of range")
```

We could then possibly deprecate `__getitem__` access.

Code after:
        Sequence,
    )

    from pandas._typing import (
        ArrayLike,
        BlockManager,
        CorrelationMethod,
        IndexLabel,
        Manager,
        SingleBlockManager,
        TakeIndexer,
    )

    from pandas import Categorical
    from pandas.core.generic import NDFrame

# TODO(typing) the return value on this callable should be any *scalar*.
AggScalar: TypeAlias = str | Callable[..., Any]
# TODO: validate types on ScalarResult and move to _typing
# Blocked from using by https://github.com/python/mypy/issues/1484
# See note at _mangle_lambda_list
ScalarResult = TypeVar("ScalarResult")


@set_module("pandas")
@dataclasses.dataclass
class NamedAgg:
    """
    Helper for column specific aggregation with control over output column names.

    Parameters
    ----------
    column : Hashable
        Column label in the DataFrame to apply aggfunc.
    aggfunc : function or str
        Function to apply to the provided column. If string, the name of a built-in
        pandas function.
    *args, **kwargs : Any
        Optional positional and keyword arguments passed to ``aggfunc``.

    See Also
    --------
    DataFrame.groupby : Group DataFrame using a mapper or by a Series of columns.

    Examples
    --------
    >>> df = pd.DataFrame({"key": [1, 1, 2], "a": [-1, 0, 1], 1: [10, 11, 12]})
    >>> agg_a = pd.NamedAgg(column="a", aggfunc="min")
    >>> agg_1 = pd.NamedAgg(column=1, aggfunc=lambda x: np.mean(x))
    >>> df.groupby("key").agg(result_a=agg_a, result_1=agg_1)
        result_a  result_1