Repository: narwhals-dev/narwhals

Issue:
[Bug]: pandas-like fails for no aggregations in group by context
### Describe the bug

> RuntimeError: Congrats, you entered unreachable code. Please report a bug to https://github.com/narwhals-dev/narwhals/issues.

Do I win something? Empty aggs are allowed in polars, but we enter unreachable code in pandas-like 😂

### Steps or code to reproduce the bug

```py
import narwhals as nw
import pandas as pd
import polars as pl

@nw.narwhalify
def func(df):
    return df.group_by("a").agg() 

data = {"a": [0, 0, 2, -1], "b": [1, 3, 2, None]}
```
- pandas:
    ```py
    func(pd.DataFrame(data))
    ```
     > RuntimeError: Congrats, you entered unreachable code. Please report a bug to https://github.com/narwhals-dev/narwhals/issues.

- polars:
    ```py
    func(pl.DataFrame(data))
    ```
    ```terminal
    shape: (3, 1)
    ┌─────┐
    │ a   │
    │ --- │
    │ i64 │
    ╞═════╡
    │ 2   │
    │ 0   │
    │ -1  │
    └─────┘
    ```

- pyarrow:
    ```py
    func(pa.table(data))
    ```

    ```terminal
    pyarrow.Table
    a: int64
    ----
    a: [[0,2,-1]]
    ```

### Expected results

Return the group(s) keys for pandas-like as well

### Actual results

pandas-like raises
polars returns group keys
pyarrow returns group keys

### Please run narwhals.show_version() and enter the output below.

```shell
System:
    python: 3.10.12 (main, Jul 29 2024, 16:56:48) [GCC 11.4.0]
   machine: Linux-5.15.133.1-microsoft-standard-WSL2-x86_64-with-glibc2.35

Python dependencies:
     narwhals: 1.6.4
       pandas: 2.2.2
       polars: 1.5.0
         cudf: 
        modin: 0.31.0
      pyarrow: 17.0.0
        numpy: 2.0.1
```


### Relevant log output

_No response_

Discussion:

Patch:
diff --git a/narwhals/_dask/group_by.py b/narwhals/_dask/group_by.py
index 463d6fc5..d5fbaaf9 100644
--- a/narwhals/_dask/group_by.py
+++ b/narwhals/_dask/group_by.py
@@ -72,6 +72,7 @@ class DaskLazyGroupBy:
             output_names.extend(expr._output_names)
 
         return agg_dask(
+            self._df,
             self._grouped,
             exprs,
             self._keys,
@@ -88,6 +89,7 @@ class DaskLazyGroupBy:
 
 
 def agg_dask(
+    df: DaskLazyFrame,
     grouped: Any,
     exprs: list[DaskExpr],
     keys: list[str],
@@ -99,6 +101,10 @@ def agg_dask(
     - https://github.com/rapidsai/cudf/issues/15118
     - https://github.com/rapidsai/cudf/issues/15084
     """
+    if not exprs:
+        # No aggregation provided
+        return df.select(*keys).unique(subset=keys)
+
     all_simple_aggs = True
     for expr in exprs:
         if not is_simple_aggregation(expr):
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 97a477dc..892291d5 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -80,6 +80,7 @@ class PandasLikeGroupBy:
             dataframe_is_empty=self._df._native_frame.empty,
             implementation=implementation,
             backend_version=self._df._backend_version,
+            native_namespace=self._df.__native_namespace__(),
         )
 
     def _from_native_frame(self, df: PandasLikeDataFrame) -> PandasLikeDataFrame:
@@ -114,6 +115,7 @@ def agg_pandas(  # noqa: PLR0915
     implementation: Any,
     backend_version: tuple[int, ...],
     dataframe_is_empty: bool,
+    native_namespace: Any,
 ) -> PandasLikeDataFrame:
     """
     This should be the fastpath, but cuDF is too far behind to use it.
@@ -204,9 +206,9 @@ def agg_pandas(  # noqa: PLR0915
             result_aggs = result_nunique_aggs
         elif simple_aggs and not nunique_aggs:
             result_aggs = result_simple_aggs
-        else:  # pragma: no cover
-            msg = "Congrats, you entered unreachable code. Please report a bug to https://github.com/narwhals-dev/narwhals/issues."
-            raise RuntimeError(msg)
+        else:
+            # No aggregation provided
+            result_aggs = native_namespace.DataFrame(grouped.groups.keys(), columns=keys)
         return from_dataframe(result_aggs.loc[:, output_names])
 
     if dataframe_is_empty:
diff --git a/narwhals/expr.py b/narwhals/expr.py
index cc8b8695..62e87ff7 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -1585,9 +1585,8 @@ class Expr:
         r"""
         Get the first `n` rows.
 
-        Arguments
-            n : int
-                Number of rows to return.
+        Arguments:
+            n: Number of rows to return.
 
         Examples:
             >>> import narwhals as nw
@@ -1628,9 +1627,8 @@ class Expr:
         r"""
         Get the last `n` rows.
 
-        Arguments
-            n : int
-                Number of rows to return.
+        Arguments:
+            n: Number of rows to return.
 
         Examples:
             >>> import narwhals as nw
diff --git a/narwhals/series.py b/narwhals/series.py
index 0b1645ea..d0d168c2 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -2003,9 +2003,8 @@ class Series:
         r"""
         Get the first `n` rows.
 
-        Arguments
-            n : int
-                Number of rows to return.
+        Arguments:
+            n: Number of rows to return.
 
         Examples:
             >>> import narwhals as nw
@@ -2044,9 +2043,8 @@ class Series:
         r"""
         Get the last `n` rows.
 
-        Arguments
-            n : int
-                Number of rows to return.
+        Arguments:
+            n: Number of rows to return.
 
         Examples:
             >>> import narwhals as nw
@@ -2084,7 +2082,7 @@ class Series:
         r"""
         Round underlying floating point data by `decimals` digits.
 
-        Arguments
+        Arguments:
             decimals: Number of decimals to round by.
 
         Notes:
@@ -2134,7 +2132,7 @@ class Series:
         r"""
         Get dummy/indicator variables.
 
-        Arguments
+        Arguments:
             separator: Separator/delimiter used when generating column names.
             drop_first: Remove the first category from the variable being encoded.
 


Test patch:
diff --git a/tests/series_only/to_dummy_test.py b/tests/series_only/to_dummy_test.py
index 2cf7f59c..404ac632 100644
--- a/tests/series_only/to_dummy_test.py
+++ b/tests/series_only/to_dummy_test.py
@@ -18,7 +18,9 @@ def test_to_dummies(constructor_eager: Any, sep: str) -> None:
 
 
 @pytest.mark.parametrize("sep", ["_", "-"])
-def test_to_dummies_drop_first(constructor_eager: Any, sep: str) -> None:
+def test_to_dummies_drop_first(request: Any, constructor_eager: Any, sep: str) -> None:
+    if "cudf" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
     s = nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"].alias("a")
     result = s.to_dummies(drop_first=True, separator=sep)
     expected = {f"a{sep}2": [0, 1, 0], f"a{sep}3": [0, 0, 1]}
diff --git a/tests/test_group_by.py b/tests/test_group_by.py
index 4bd3427a..6f12d06b 100644
--- a/tests/test_group_by.py
+++ b/tests/test_group_by.py
@@ -246,3 +246,10 @@ def test_key_with_nulls(constructor: Any, request: Any) -> None:
         )
         expected = {"b": [4.0, 5, float("nan")], "len": [1, 1, 1], "a": [1, 2, 3]}
         compare_dicts(result, expected)
+
+
+def test_no_agg(constructor: Any) -> None:
+    result = nw.from_native(constructor(data)).group_by(["a", "b"]).agg().sort("a", "b")
+
+    expected = {"a": [1, 3], "b": [4, 6]}
+    compare_dicts(result, expected)