Repository: narwhals-dev/narwhals
Pull request #2049 (2025-02-19 15:37:37)

refactor(typing): Avoid more `ignore[attr-defined]` on `Datetime`
## What type of PR is this? (check all applicable)

- [x] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [x] 🐳 Other

## Related issues

Made possible by
- #2025
- #1997

## Checklist

- [x] Code follows style guide (ruff)
- [ ] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below



Patch:
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index d3378ea8..3b6baee0 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -409,7 +409,13 @@ class ArrowExpr(CompliantExpr[ArrowSeries]):
             self, "clip", lower_bound=lower_bound, upper_bound=upper_bound
         )
 
-    def over(self: Self, keys: list[str]) -> Self:
+    def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
+        if kind is ExprKind.TRANSFORM:
+            msg = (
+                "Elementwise operations in `over` context are not supported for PyArrow."
+            )
+            raise NotImplementedError(msg)
+
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
             output_names, aliases = evaluate_output_names_and_aliases(self, df, [])
             if overlap := set(output_names).intersection(keys):
diff --git a/narwhals/_arrow/series_dt.py b/narwhals/_arrow/series_dt.py
index 3ba67752..6b45a0fa 100644
--- a/narwhals/_arrow/series_dt.py
+++ b/narwhals/_arrow/series_dt.py
@@ -9,7 +9,6 @@ import pyarrow.compute as pc
 from narwhals._arrow.utils import floordiv_compat
 from narwhals._arrow.utils import lit
 from narwhals.utils import import_dtypes_module
-from narwhals.utils import isinstance_or_issubclass
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -52,7 +51,7 @@ class ArrowSeriesDateTimeNamespace:
     def timestamp(self: Self, time_unit: TimeUnit) -> ArrowSeries:
         ser: ArrowSeries = self._compliant_series
         dtypes = import_dtypes_module(ser._version)
-        if isinstance_or_issubclass(ser.dtype, dtypes.Datetime):
+        if isinstance(ser.dtype, dtypes.Datetime):
             unit = ser.dtype.time_unit
             s_cast = ser._native_series.cast(pa.int64())
             if unit == "ns":
@@ -86,7 +85,7 @@ class ArrowSeriesDateTimeNamespace:
             else:  # pragma: no cover
                 msg = f"unexpected time unit {unit}, please report an issue at https://github.com/narwhals-dev/narwhals"
                 raise AssertionError(msg)
-        elif isinstance_or_issubclass(ser.dtype, dtypes.Date):
+        elif isinstance(ser.dtype, dtypes.Date):
             time_s = pc.multiply(ser._native_series.cast(pa.int32()), 86400)
             if time_unit == "ns":
                 result = cast("ArrowChunkedArray", pc.multiply(time_s, 1_000_000_000))
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index a0af9e64..c4aac7ca 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -535,7 +535,7 @@ class DaskExpr(CompliantExpr["dx.Series"]):
             lambda _input: _input.isna().sum().to_series(), "null_count"
         )
 
-    def over(self: Self, keys: list[str]) -> Self:
+    def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
         def func(df: DaskLazyFrame) -> list[Any]:
             output_names, aliases = evaluate_output_names_and_aliases(self, df, [])
             if overlap := set(output_names).intersection(keys):
diff --git a/narwhals/_dask/expr_dt.py b/narwhals/_dask/expr_dt.py
index 330933bc..c569d7dc 100644
--- a/narwhals/_dask/expr_dt.py
+++ b/narwhals/_dask/expr_dt.py
@@ -114,8 +114,8 @@ class DaskExprDateTimeNamespace:
                 # Date is only supported in pandas dtypes if pyarrow-backed
                 s_cast = s.astype("Int32[pyarrow]")
                 result = calculate_timestamp_date(s_cast, time_unit)
-            elif dtype == dtypes.Datetime:
-                original_time_unit = dtype.time_unit  # type: ignore[attr-defined]
+            elif isinstance(dtype, dtypes.Datetime):
+                original_time_unit = dtype.time_unit
                 s_cast = (
                     s.astype("Int64[pyarrow]") if is_pyarrow_dtype else s.astype("int64")
                 )
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index f9694c07..773c281f 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -419,7 +419,7 @@ class PandasLikeExpr(CompliantExpr[PandasLikeSeries]):
             kwargs={**self._kwargs, "name": name},
         )
 
-    def over(self: Self, keys: list[str]) -> Self:
+    def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
         if (
             is_simple_aggregation(self)
             and (function_name := re.sub(r"(\w+->)", "", self._function_name))
@@ -469,7 +469,13 @@ class PandasLikeExpr(CompliantExpr[PandasLikeSeries]):
                     )
                 )
                 return [result_frame[name] for name in aliases]
-
+        elif kind is ExprKind.TRANSFORM:
+            msg = (
+                "Elementwise operations are only supported in `over` context "
+                "for pandas if they are elementary "
+                "(e.g. `nw.col('a').cum_sum().over('b'))`)."
+            )
+            raise NotImplementedError(msg)
         else:
 
             def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index c45bd6aa..ee07aa26 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -656,7 +656,7 @@ class PandasLikeSeries(CompliantSeries):
         # https://numpy.org/doc/stable/reference/generated/numpy.ndarray.__array__.html
         copy = copy or self._implementation is Implementation.CUDF
         dtypes = import_dtypes_module(self._version)
-        if self.dtype == dtypes.Datetime and self.dtype.time_zone is not None:  # type: ignore[attr-defined]
+        if isinstance(self.dtype, dtypes.Datetime) and self.dtype.time_zone is not None:
             s = self.dt.convert_time_zone("UTC").dt.replace_time_zone(None)._native_series
         else:
             s = self._native_series
diff --git a/narwhals/_pandas_like/series_dt.py b/narwhals/_pandas_like/series_dt.py
index bbebbc6b..c16be61a 100644
--- a/narwhals/_pandas_like/series_dt.py
+++ b/narwhals/_pandas_like/series_dt.py
@@ -218,8 +218,8 @@ class PandasLikeSeriesDateTimeNamespace:
             # Date is only supported in pandas dtypes if pyarrow-backed
             s_cast = s.astype("Int32[pyarrow]")
             result = calculate_timestamp_date(s_cast, time_unit)
-        elif dtype == dtypes.Datetime:
-            original_time_unit = dtype.time_unit  # type: ignore[attr-defined]
+        elif isinstance(dtype, dtypes.Datetime):
+            original_time_unit = dtype.time_unit
             if (
                 self._compliant_series._implementation is Implementation.PANDAS
                 and self._compliant_series._backend_version < (2,)
diff --git a/narwhals/_polars/expr.py b/narwhals/_polars/expr.py
index 52afb834..d39d2cdb 100644
--- a/narwhals/_polars/expr.py
+++ b/narwhals/_polars/expr.py
@@ -97,6 +97,9 @@ class PolarsExpr:
             )
         return self._from_native_expr(self._native_expr.is_nan())
 
+    def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
+        return self._from_native_expr(self._native_expr.over(keys))
+
     def rolling_var(
         self: Self,
         window_size: int,
diff --git a/narwhals/_spark_like/expr.py b/narwhals/_spark_like/expr.py
index 97c17606..dc4db7c6 100644
--- a/narwhals/_spark_like/expr.py
+++ b/narwhals/_spark_like/expr.py
@@ -454,7 +454,7 @@ class SparkLikeExpr(CompliantExpr["Column"]):
 
         return self._from_call(_n_unique, "n_unique")
 
-    def over(self: Self, keys: list[str]) -> Self:
+    def over(self: Self, keys: list[str], kind: ExprKind) -> Self:
         def func(df: SparkLikeLazyFrame) -> list[Column]:
             return [expr.over(self._Window.partitionBy(*keys)) for expr in self._call(df)]
 
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 2c323329..67b7cfdd 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -1602,7 +1602,9 @@ class Expr:
             msg = "`.over()` can not be used for expressions which change length."
             raise LengthChangingExprError(msg)
         return self.__class__(
-            lambda plx: self._to_compliant_expr(plx).over(flatten(keys)),
+            lambda plx: self._to_compliant_expr(plx).over(
+                flatten(keys), kind=self._metadata["kind"]
+            ),
             ExprMetadata(
                 kind=ExprKind.TRANSFORM,
                 is_order_dependent=self._metadata["is_order_dependent"],
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 8d5719c3..22634f98 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -320,12 +320,12 @@ def from_dict(
         backend: specifies which eager backend instantiate to. Only
             necessary if inputs are not Narwhals Series.
 
-                `backend` can be specified in various ways:
+            `backend` can be specified in various ways:
 
-                - As `Implementation.<BACKEND>` with `BACKEND` being `PANDAS`, `PYARROW`,
-                    `POLARS`, `MODIN` or `CUDF`.
-                - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`.
-                - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`.
+            - As `Implementation.<BACKEND>` with `BACKEND` being `PANDAS`, `PYARROW`,
+                `POLARS`, `MODIN` or `CUDF`.
+            - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`.
+            - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`.
         native_namespace: The native library to use for DataFrame creation.
 
             **Deprecated** (v1.26.0):
@@ -743,26 +743,37 @@ def get_level(
 def read_csv(
     source: str,
     *,
-    native_namespace: ModuleType,
+    backend: ModuleType | Implementation | str | None = None,
+    native_namespace: ModuleType | None = None,
     **kwargs: Any,
 ) -> DataFrame[Any]:
     """Read a CSV file into a DataFrame.
 
     Arguments:
         source: Path to a file.
+        backend: The eager backend for DataFrame creation.
+            `backend` can be specified in various ways:
+
+            - As `Implementation.<BACKEND>` with `BACKEND` being `PANDAS`, `PYARROW`,
+                `POLARS`, `MODIN` or `CUDF`.
+            - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`.
+            - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`.
         native_namespace: The native library to use for DataFrame creation.
+
+            **Deprecated** (v1.27.2):
+                Please use `backend` instead. Note that `native_namespace` is still available
+                (and won't emit a deprecation warning) if you use `narwhals.stable.v1`,
+                see [perfect backwards compatibility policy](../backcompat.md/).
         kwargs: Extra keyword arguments which are passed to the native CSV reader.
             For example, you could use
-            `nw.read_csv('file.csv', native_namespace=pd, engine='pyarrow')`.
+            `nw.read_csv('file.csv', backend='pandas', engine='pyarrow')`.
 
     Returns:
         DataFrame.
 
     Examples:
-        >>> import pandas as pd
         >>> import narwhals as nw
-        >>>
-        >>> nw.read_csv("file.csv", native_namespace=pd)  # doctest:+SKIP
+        >>> nw.read_csv("file.csv", backend="pandas")  # doctest:+SKIP
         ┌──────────────────┐
         |Narwhals DataFrame|
         |------------------|
@@ -771,21 +782,28 @@ def read_csv(
         |     1  2   5     |
         └──────────────────┘
     """
-    return _read_csv_impl(source, native_namespace=native_namespace, **kwargs)
+    backend = validate_native_namespace_and_backend(
+        backend, native_namespace, emit_deprecation_warning=True
+    )
+    if backend is None:  # pragma: no cover
+        msg = "`backend` must be specified in `read_csv`."
+        raise ValueError(msg)
+    return _read_csv_impl(source, backend=backend, **kwargs)
 
 
 def _read_csv_impl(
-    source: str, *, native_namespace: ModuleType, **kwargs: Any
+    source: str, *, backend: ModuleType | Implementation | str, **kwargs: Any
 ) -> DataFrame[Any]:
-    implementation = Implementation.from_native_namespace(native_namespace)
-    if implementation in (
+    eager_backend = Implementation.from_backend(backend)
+    native_namespace = eager_backend.to_native_namespace()
+    if eager_backend in (
         Implementation.POLARS,
         Implementation.PANDAS,
         Implementation.MODIN,
         Implementation.CUDF,
     ):
         native_frame = native_namespace.read_csv(source, **kwargs)
-    elif implementation is Implementation.PYARROW:
+    elif eager_backend is Implementation.PYARROW:
         from pyarrow import csv  # ignore-banned-import
 
         native_frame = csv.read_csv(source, **kwargs)
diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py
index 5a64fb1a..5291e125 100644
--- a/narwhals/stable/v1/__init__.py
+++ b/narwhals/stable/v1/__init__.py
@@ -2191,12 +2191,12 @@ def from_dict(
         backend: specifies which eager backend instantiate to. Only
             necessary if inputs are not Narwhals Series.
 
-                `backend` can be specified in various ways:
+            `backend` can be specified in various ways:
 
-                - As `Implementation.<BACKEND>` with `BACKEND` being `PANDAS`, `PYARROW`,
-                    `POLARS`, `MODIN` or `CUDF`.
-                - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`.
-                - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`.
+            - As `Implementation.<BACKEND>` with `BACKEND` being `PANDAS`, `PYARROW`,
+                `POLARS`, `MODIN` or `CUDF`.
+            - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`.
+            - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`.
         native_namespace: The native library to use for DataFrame creation.
 
             **Deprecated** (v1.26.0):
@@ -2241,22 +2241,43 @@ def from_numpy(
 
 
 def read_csv(
-    source: str, *, native_namespace: ModuleType, **kwargs: Any
+    source: str,
+    *,
+    backend: ModuleType | Implementation | str | None = None,
+    native_namespace: ModuleType | None = None,
+    **kwargs: Any,
 ) -> DataFrame[Any]:
     """Read a CSV file into a DataFrame.
 
     Arguments:
         source: Path to a file.
+        backend: The eager backend for DataFrame creation.
+            `backend` can be specified in various ways:
+
+            - As `Implementation.<BACKEND>` with `BACKEND` being `PANDAS`, `PYARROW`,
+                `POLARS`, `MODIN` or `CUDF`.
+            - As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`.
+            - Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`.
         native_namespace: The native library to use for DataFrame creation.
+
+            **Deprecated** (v1.27.2):
+                Please use `backend` instead. Note that `native_namespace` is still available
+                (and won't emit a deprecation warning) if you use `narwhals.stable.v1`,
+                see [perfect backwards compatibility policy](../backcompat.md/).
         kwargs: Extra keyword arguments which are passed to the native CSV reader.
             For example, you could use
-            `nw.read_csv('file.csv', native_namespace=pd, engine='pyarrow')`.
+            `nw.read_csv('file.csv', backend='pandas', engine='pyarrow')`.
 
     Returns:
         DataFrame.
     """
+    backend = validate_native_namespace_and_backend(
+        backend, native_namespace, emit_deprecation_warning=True
+    )
+    if backend is None:  # pragma: no cover
+        raise AssertionError
     return _stableify(  # type: ignore[no-any-return]
-        _read_csv_impl(source, native_namespace=native_namespace, **kwargs)
+        _read_csv_impl(source, backend=backend, **kwargs)
     )
 
 
diff --git a/narwhals/utils.py b/narwhals/utils.py
index cb33a603..d623b7dd 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -1294,11 +1294,11 @@ def dtype_matches_time_unit_and_time_zone(
     dtype: DType, dtypes: DTypes, time_units: Set[TimeUnit], time_zones: Set[str | None]
 ) -> bool:
     return (
-        (dtype == dtypes.Datetime)
-        and (dtype.time_unit in time_units)  # type: ignore[attr-defined]
+        isinstance(dtype, dtypes.Datetime)
+        and (dtype.time_unit in time_units)
         and (
-            dtype.time_zone in time_zones  # type: ignore[attr-defined]
-            or ("*" in time_zones and dtype.time_zone is not None)  # type: ignore[attr-defined]
+            dtype.time_zone in time_zones
+            or ("*" in time_zones and dtype.time_zone is not None)
         )
     )
 


Test patch:
diff --git a/tests/expr_and_series/dt/convert_time_zone_test.py b/tests/expr_and_series/dt/convert_time_zone_test.py
index 9a18ee07..9effb17e 100644
--- a/tests/expr_and_series/dt/convert_time_zone_test.py
+++ b/tests/expr_and_series/dt/convert_time_zone_test.py
@@ -42,7 +42,8 @@ def test_convert_time_zone(
     result = df.select(nw.col("a").dt.convert_time_zone("Asia/Kathmandu"))
     result_dtype = result.collect_schema()["a"]
     assert result_dtype == nw.Datetime
-    assert result_dtype.time_zone == "Asia/Kathmandu"  # type: ignore[attr-defined]
+    assert isinstance(result_dtype, nw.Datetime)
+    assert result_dtype.time_zone == "Asia/Kathmandu"
     result_str = result.select(nw.col("a").dt.to_string("%Y-%m-%dT%H:%M%z"))
     expected = {"a": ["2020-01-01T05:45+0545", "2020-01-02T05:45+0545"]}
     assert_equal_data(result_str, expected)
@@ -70,7 +71,8 @@ def test_convert_time_zone_series(
     result = df.select(df["a"].dt.convert_time_zone("Asia/Kathmandu"))
     result_dtype = result.collect_schema()["a"]
     assert result_dtype == nw.Datetime
-    assert result_dtype.time_zone == "Asia/Kathmandu"  # type: ignore[attr-defined]
+    assert isinstance(result_dtype, nw.Datetime)
+    assert result_dtype.time_zone == "Asia/Kathmandu"
     result_str = result.select(nw.col("a").dt.to_string("%Y-%m-%dT%H:%M%z"))
     expected = {"a": ["2020-01-01T05:45+0545", "2020-01-02T05:45+0545"]}
     assert_equal_data(result_str, expected)
@@ -105,7 +107,8 @@ def test_convert_time_zone_from_none(
     )
     result_dtype = result.collect_schema()["a"]
     assert result_dtype == nw.Datetime
-    assert result_dtype.time_zone == "Asia/Kathmandu"  # type: ignore[attr-defined]
+    assert isinstance(result_dtype, nw.Datetime)
+    assert result_dtype.time_zone == "Asia/Kathmandu"
     result_str = result.select(nw.col("a").dt.to_string("%Y-%m-%dT%H:%M%z"))
     expected = {"a": ["2020-01-01T05:45+0545", "2020-01-02T05:45+0545"]}
     assert_equal_data(result_str, expected)
diff --git a/tests/expr_and_series/dt/replace_time_zone_test.py b/tests/expr_and_series/dt/replace_time_zone_test.py
index 6876c318..b8a78bc2 100644
--- a/tests/expr_and_series/dt/replace_time_zone_test.py
+++ b/tests/expr_and_series/dt/replace_time_zone_test.py
@@ -40,7 +40,8 @@ def test_replace_time_zone(
     result = df.select(nw.col("a").dt.replace_time_zone("Asia/Kathmandu"))
     result_dtype = result.collect_schema()["a"]
     assert result_dtype == nw.Datetime
-    assert result_dtype.time_zone == "Asia/Kathmandu"  # type: ignore[attr-defined]
+    assert isinstance(result_dtype, nw.Datetime)
+    assert result_dtype.time_zone == "Asia/Kathmandu"
     result_str = result.select(nw.col("a").dt.to_string("%Y-%m-%dT%H:%M%z"))
     expected = {"a": ["2020-01-01T00:00+0545", "2020-01-02T00:00+0545"]}
     assert_equal_data(result_str, expected)
@@ -68,7 +69,8 @@ def test_replace_time_zone_none(
     result = df.select(nw.col("a").dt.replace_time_zone(None))
     result_dtype = result.collect_schema()["a"]
     assert result_dtype == nw.Datetime
-    assert result_dtype.time_zone is None  # type: ignore[attr-defined]
+    assert isinstance(result_dtype, nw.Datetime)
+    assert result_dtype.time_zone is None
     result_str = result.select(nw.col("a").dt.to_string("%Y-%m-%dT%H:%M"))
     expected = {"a": ["2020-01-01T00:00", "2020-01-02T00:00"]}
     assert_equal_data(result_str, expected)
@@ -95,7 +97,8 @@ def test_replace_time_zone_series(
     result = df.select(df["a"].dt.replace_time_zone("Asia/Kathmandu"))
     result_dtype = result.collect_schema()["a"]
     assert result_dtype == nw.Datetime
-    assert result_dtype.time_zone == "Asia/Kathmandu"  # type: ignore[attr-defined]
+    assert isinstance(result_dtype, nw.Datetime)
+    assert result_dtype.time_zone == "Asia/Kathmandu"
     result_str = result.select(nw.col("a").dt.to_string("%Y-%m-%dT%H:%M%z"))
     expected = {"a": ["2020-01-01T00:00+0545", "2020-01-02T00:00+0545"]}
     assert_equal_data(result_str, expected)
@@ -121,7 +124,8 @@ def test_replace_time_zone_none_series(
     result = df.select(df["a"].dt.replace_time_zone(None))
     result_dtype = result.collect_schema()["a"]
     assert result_dtype == nw.Datetime
-    assert result_dtype.time_zone is None  # type: ignore[attr-defined]
+    assert isinstance(result_dtype, nw.Datetime)
+    assert result_dtype.time_zone is None
     result_str = result.select(df["a"].dt.to_string("%Y-%m-%dT%H:%M"))
     expected = {"a": ["2020-01-01T00:00", "2020-01-02T00:00"]}
     assert_equal_data(result_str, expected)
diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py
index 600bde17..65dba7f6 100644
--- a/tests/expr_and_series/over_test.py
+++ b/tests/expr_and_series/over_test.py
@@ -4,6 +4,7 @@ import re
 from contextlib import nullcontext as does_not_raise
 
 import pandas as pd
+import pyarrow as pa
 import pytest
 
 import narwhals.stable.v1 as nw
@@ -259,3 +260,13 @@ def test_over_raise_len_change(constructor: Constructor) -> None:
         match=re.escape("`.over()` can not be used for expressions which change length."),
     ):
         nw.from_native(df).select(nw.col("b").drop_nulls().over("a"))
+
+
+def test_unsupported_over() -> None:
+    data = {"a": [1, 2, 3, 4, 5, 6], "b": ["x", "x", "x", "y", "y", "y"]}
+    df = pd.DataFrame(data)
+    with pytest.raises(NotImplementedError, match="elementary"):
+        nw.from_native(df).select(nw.col("a").shift(1).cum_sum().over("b"))
+    tbl = pa.table(data)  # type: ignore[arg-type]
+    with pytest.raises(NotImplementedError, match="Elementwise"):
+        nw.from_native(tbl).select(nw.col("a").shift(1).cum_sum().over("b"))
diff --git a/tests/group_by_test.py b/tests/group_by_test.py
index 3ec5ecda..869544cd 100644
--- a/tests/group_by_test.py
+++ b/tests/group_by_test.py
@@ -6,6 +6,7 @@ from typing import Mapping
 
 import pandas as pd
 import polars as pl
+import pyarrow as pa
 import pytest
 
 import narwhals.stable.v1 as nw
@@ -31,6 +32,10 @@ def test_group_by_complex() -> None:
             df.group_by("a").agg((nw.col("b") - nw.col("c").mean()).mean()).sort("a")
         )
     assert_equal_data(result_pd, expected)
+    with pytest.raises(ValueError, match="complex aggregation"):
+        nw.from_native(pa.table({"a": [1, 1, 2], "b": [4, 5, 6]})).group_by("a").agg(
+            (nw.col("b") - nw.col("c").mean()).mean()
+        )
 
     lf = nw.from_native(df_lazy).lazy()
     result_pl = lf.group_by("a").agg((nw.col("b") - nw.col("c").mean()).mean()).sort("a")
diff --git a/tests/read_scan_test.py b/tests/read_scan_test.py
index 46c6d732..1f72250b 100644
--- a/tests/read_scan_test.py
+++ b/tests/read_scan_test.py
@@ -9,37 +9,50 @@ import pytest
 
 import narwhals as nw
 import narwhals.stable.v1 as nw_v1
+from narwhals.utils import Implementation
 from tests.utils import PANDAS_VERSION
 from tests.utils import Constructor
 from tests.utils import ConstructorEager
 from tests.utils import assert_equal_data
 
 data: Mapping[str, Any] = {"a": [1, 2, 3], "b": [4.5, 6.7, 8.9], "z": ["x", "y", "w"]}
-
-
+TEST_EAGER_BACKENDS = [
+    Implementation.POLARS,
+    Implementation.PANDAS,
+    Implementation.PYARROW,
+    "polars",
+    "pandas",
+    "pyarrow",
+]
+
+
+@pytest.mark.parametrize(
+    "backend",
+    TEST_EAGER_BACKENDS,
+)
 def test_read_csv(
     tmpdir: pytest.TempdirFactory,
-    constructor_eager: ConstructorEager,
+    backend: Implementation | str,
 ) -> None:
     df_pl = pl.DataFrame(data)
     filepath = str(tmpdir / "file.csv")  # type: ignore[operator]
     df_pl.write_csv(filepath)
-    df = nw.from_native(constructor_eager(data))
-    native_namespace = nw.get_native_namespace(df)
-    result = nw.read_csv(filepath, native_namespace=native_namespace)
+    result = nw.read_csv(filepath, backend=backend)
     assert_equal_data(result, data)
     assert isinstance(result, nw.DataFrame)
 
 
+@pytest.mark.parametrize(
+    "backend",
+    TEST_EAGER_BACKENDS,
+)
 def test_read_csv_v1(
-    tmpdir: pytest.TempdirFactory, constructor_eager: ConstructorEager
+    tmpdir: pytest.TempdirFactory, backend: Implementation | str
 ) -> None:
     df_pl = pl.DataFrame(data)
     filepath = str(tmpdir / "file.csv")  # type: ignore[operator]
     df_pl.write_csv(filepath)
-    df = nw_v1.from_native(constructor_eager(data))
-    native_namespace = nw_v1.get_native_namespace(df)
-    result = nw_v1.read_csv(filepath, native_namespace=native_namespace)
+    result = nw_v1.read_csv(filepath, backend=backend)
     assert_equal_data(result, data)
     assert isinstance(result, nw_v1.DataFrame)
 
@@ -49,7 +62,7 @@ def test_read_csv_kwargs(tmpdir: pytest.TempdirFactory) -> None:
     df_pl = pl.DataFrame(data)
     filepath = str(tmpdir / "file.csv")  # type: ignore[operator]
     df_pl.write_csv(filepath)
-    result = nw.read_csv(filepath, native_namespace=pd, engine="pyarrow")
+    result = nw.read_csv(filepath, backend=pd, engine="pyarrow")
     assert_equal_data(result, data)