Repository: narwhals-dev/narwhals
Pull request #671 (2024-07-29 16:33:21)

bug: fix `iter_rows` inconsistency in pandas
<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [x] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue # 
- Closes #

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.

While using `narwhals` in a small library of mine, I noticed a little inconsistency with `PandasLikeDataFrame.iter_rows` when the columns are "renamed to positional names if they are invalid Python identifiers, repeated, or start with an underscore." ([pandas docs](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.itertuples.html))

To reproduce:
```python
import narwhals as nw
import pandas as pd
import polars as pl

df = pd.DataFrame(
    [[1, 2, 3,4], [6, 7, 8, 9]],
    columns=["a", "_b", 1, "2"]
)
pl_df = pl.DataFrame(df)

def func(df):
    df = nw.from_native(df)
    return list(df.iter_rows(named=True))

func(df)
# [{'a': 1, '_1': 2, '_2': 3, '_3': 4}, {'a': 6, '_1': 7, '_2': 8, '_3': 9}]
func(pl_df)
# [{'a': 1, '_b': 2, '1': 3, '2': 4}, {'a': 6, '_b': 7, '1': 8, '2': 9}]
```

This should fix that 👀


Patch:
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 8478e92d..21588616 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -196,9 +196,11 @@ class PandasLikeDataFrame:
         if not named:
             yield from self._native_dataframe.itertuples(index=False, name=None)
         else:
+            col_names = self._native_dataframe.columns
             yield from (
-                row._asdict() for row in self._native_dataframe.itertuples(index=False)
-            )
+                dict(zip(col_names, row))
+                for row in self._native_dataframe.itertuples(index=False)
+            )  # type: ignore[misc]
 
     @property
     def schema(self) -> dict[str, DType]:


Test patch:
diff --git a/tests/dask_test.py b/tests/dask_test.py
index 24d0fb0a..4e3b347d 100644
--- a/tests/dask_test.py
+++ b/tests/dask_test.py
@@ -18,6 +18,7 @@ import pandas as pd
 import pytest
 
 import narwhals.stable.v1 as nw
+from narwhals.utils import parse_version
 from tests.utils import compare_dicts
 
 pytest.importorskip("dask")
@@ -221,10 +222,18 @@ def test_to_datetime() -> None:
     ],
 )
 def test_str_to_uppercase(
+    request: pytest.FixtureRequest,
     data: dict[str, list[str]],
     expected: dict[str, list[str]],
 ) -> None:
     import dask.dataframe as dd
+    import pyarrow as pa
+
+    if (parse_version(pa.__version__) < (12, 0, 0)) and ("ß" in data["a"][0]):
+        # We are marking it xfail for these conditions above
+        # since the pyarrow backend will convert
+        # smaller cap 'ß' to upper cap 'ẞ' instead of 'SS'
+        request.applymarker(pytest.mark.xfail)
 
     dfdd = dd.from_pandas(pd.DataFrame(data))
     df = nw.from_native(dfdd)
diff --git a/tests/frame/rows_test.py b/tests/frame/rows_test.py
index 263671ff..e3123f19 100644
--- a/tests/frame/rows_test.py
+++ b/tests/frame/rows_test.py
@@ -43,13 +43,13 @@ df_polars_na = pl.DataFrame({"a": [None, 3, 2], "b": [4, 4, 6], "z": [7.0, None,
 @pytest.mark.parametrize(
     ("named", "expected"),
     [
-        (False, [(1, 4, 7.0), (3, 4, 8.0), (2, 6, 9.0)]),
+        (False, [(1, 4, 7.0, 5), (3, 4, 8.0, 6), (2, 6, 9.0, 7)]),
         (
             True,
             [
-                {"a": 1, "b": 4, "z": 7.0},
-                {"a": 3, "b": 4, "z": 8.0},
-                {"a": 2, "b": 6, "z": 9.0},
+                {"a": 1, "_b": 4, "z": 7.0, "1": 5},
+                {"a": 3, "_b": 4, "z": 8.0, "1": 6},
+                {"a": 2, "_b": 6, "z": 9.0, "1": 7},
             ],
         ),
     ],
@@ -59,7 +59,7 @@ def test_iter_rows(
     named: bool,  # noqa: FBT001
     expected: list[tuple[Any, ...]] | list[dict[str, Any]],
 ) -> None:
-    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
+    data = {"a": [1, 3, 2], "_b": [4, 4, 6], "z": [7.0, 8, 9], "1": [5, 6, 7]}
     df = nw.from_native(constructor_eager(data), eager_only=True)
     result = list(df.iter_rows(named=named))
     assert result == expected