Repository: narwhals-dev/narwhals
Pull request #1845 (2025-01-21 11:15:37)

chore: validate expressions shape at narwhals/expr.py level
<!--
# 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
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Related issue #\<issue number\>
- Closes #\<issue number\>

## Checklist

- [ ] 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/dataframe.py b/narwhals/_arrow/dataframe.py
index f36fbadb..cd1b2a05 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -12,11 +12,11 @@ from typing import overload
 import pyarrow as pa
 import pyarrow.compute as pc
 
+from narwhals._arrow.utils import broadcast_and_extract_dataframe_comparand
 from narwhals._arrow.utils import broadcast_series
 from narwhals._arrow.utils import convert_str_slice_to_int_slice
 from narwhals._arrow.utils import native_to_narwhals_dtype
 from narwhals._arrow.utils import select_rows
-from narwhals._arrow.utils import validate_dataframe_comparand
 from narwhals._expression_parsing import evaluate_into_exprs
 from narwhals.dependencies import is_numpy_array
 from narwhals.utils import Implementation
@@ -311,12 +311,8 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
         for col_value in new_columns:
             col_name = col_value.name
 
-            column = validate_dataframe_comparand(
-                length=length,
-                other=col_value,
-                backend_version=self._backend_version,
-                allow_broadcast=True,
-                method_name="with_columns",
+            column = broadcast_and_extract_dataframe_comparand(
+                length=length, other=col_value, backend_version=self._backend_version
             )
 
             native_frame = (
@@ -494,12 +490,8 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
             )
             # `[0]` is safe as all_horizontal's expression only returns a single column
             mask = expr._call(self)[0]
-            mask_native = validate_dataframe_comparand(
-                length=len(self),
-                other=mask,
-                backend_version=self._backend_version,
-                allow_broadcast=False,
-                method_name="filter",
+            mask_native = broadcast_and_extract_dataframe_comparand(
+                length=len(self), other=mask, backend_version=self._backend_version
             )
         return self._from_native_frame(self._native_frame.filter(mask_native))
 
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index d1e66674..2040c4e2 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -9,7 +9,6 @@ from typing import overload
 import pyarrow as pa
 import pyarrow.compute as pc
 
-from narwhals.exceptions import ShapeError
 from narwhals.utils import import_dtypes_module
 from narwhals.utils import isinstance_or_issubclass
 
@@ -207,13 +206,10 @@ def broadcast_and_extract_native(
     return lhs._native_series, rhs
 
 
-def validate_dataframe_comparand(
+def broadcast_and_extract_dataframe_comparand(
     length: int,
     other: Any,
     backend_version: tuple[int, ...],
-    *,
-    allow_broadcast: bool,
-    method_name: str,
 ) -> Any:
     """Validate RHS of binary operation.
 
@@ -225,12 +221,6 @@ def validate_dataframe_comparand(
     if isinstance(other, ArrowSeries):
         len_other = len(other)
         if len_other == 1:
-            if length > 1 and not allow_broadcast:
-                msg = (
-                    f"{method_name}'s length: 1 differs from that of the series: {length}"
-                )
-                raise ShapeError(msg)
-
             import numpy as np  # ignore-banned-import
 
             value = other._native_series[0]
@@ -238,10 +228,6 @@ def validate_dataframe_comparand(
                 value = value.as_py()
             return pa.array(np.full(shape=length, fill_value=value))
 
-        if length != len_other:
-            msg = f"{method_name}'s length: {len_other} differs from that of the series: {length}"
-            raise ShapeError(msg)
-
         return other._native_series
 
     from narwhals._arrow.dataframe import ArrowDataFrame  # pragma: no cover
diff --git a/narwhals/_duckdb/namespace.py b/narwhals/_duckdb/namespace.py
index c68bf91d..3b23bd05 100644
--- a/narwhals/_duckdb/namespace.py
+++ b/narwhals/_duckdb/namespace.py
@@ -258,6 +258,34 @@ class DuckDBNamespace(CompliantNamespace["duckdb.Expression"]):
             kwargs={"exprs": exprs},
         )
 
+    def mean_horizontal(self, *exprs: IntoDuckDBExpr) -> DuckDBExpr:
+        parsed_exprs = parse_into_exprs(*exprs, namespace=self)
+
+        def func(df: DuckDBLazyFrame) -> list[duckdb.Expression]:
+            cols = [c for _expr in parsed_exprs for c in _expr(df)]
+            col_name = get_column_name(df, cols[0])
+            return [
+                (
+                    reduce(
+                        operator.add,
+                        (CoalesceOperator(col, ConstantExpression(0)) for col in cols),
+                    )
+                    / reduce(operator.add, (col.isnotnull().cast("int") for col in cols))
+                ).alias(col_name)
+            ]
+
+        return DuckDBExpr(
+            call=func,
+            depth=max(x._depth for x in parsed_exprs) + 1,
+            function_name="mean_horizontal",
+            root_names=combine_root_names(parsed_exprs),
+            output_names=reduce_output_names(parsed_exprs),
+            returns_scalar=False,
+            backend_version=self._backend_version,
+            version=self._version,
+            kwargs={"exprs": exprs},
+        )
+
     def when(
         self,
         *predicates: IntoDuckDBExpr,
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 05802b6a..88e1261b 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -10,6 +10,7 @@ from typing import Sequence
 from typing import overload
 
 from narwhals._expression_parsing import evaluate_into_exprs
+from narwhals._pandas_like.utils import broadcast_and_extract_dataframe_comparand
 from narwhals._pandas_like.utils import broadcast_series
 from narwhals._pandas_like.utils import convert_str_slice_to_int_slice
 from narwhals._pandas_like.utils import create_compliant_series
@@ -18,7 +19,6 @@ from narwhals._pandas_like.utils import native_to_narwhals_dtype
 from narwhals._pandas_like.utils import pivot_table
 from narwhals._pandas_like.utils import rename
 from narwhals._pandas_like.utils import select_columns_by_name
-from narwhals._pandas_like.utils import validate_dataframe_comparand
 from narwhals.dependencies import is_numpy_array
 from narwhals.utils import Implementation
 from narwhals.utils import check_column_exists
@@ -417,11 +417,8 @@ class PandasLikeDataFrame:
             )
             # `[0]` is safe as all_horizontal's expression only returns a single column
             mask = expr._call(self)[0]
-            mask_native = validate_dataframe_comparand(
-                self._native_frame.index,
-                mask,
-                allow_broadcast=False,
-                method_name="filter",
+            mask_native = broadcast_and_extract_dataframe_comparand(
+                self._native_frame.index, mask
             )
 
         return self._from_native_frame(self._native_frame.loc[mask_native])
@@ -442,21 +439,15 @@ class PandasLikeDataFrame:
         for name in self._native_frame.columns:
             if name in new_column_name_to_new_column_map:
                 to_concat.append(
-                    validate_dataframe_comparand(
-                        index,
-                        new_column_name_to_new_column_map.pop(name),
-                        allow_broadcast=True,
-                        method_name="with_columns",
+                    broadcast_and_extract_dataframe_comparand(
+                        index, new_column_name_to_new_column_map.pop(name)
                     )
                 )
             else:
                 to_concat.append(self._native_frame[name])
         to_concat.extend(
-            validate_dataframe_comparand(
-                index,
-                new_column_name_to_new_column_map[s],
-                allow_broadcast=True,
-                method_name="with_columns",
+            broadcast_and_extract_dataframe_comparand(
+                index, new_column_name_to_new_column_map[s]
             )
             for s in new_column_name_to_new_column_map
         )
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index 84efef83..3b6f9efa 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -18,7 +18,6 @@ from narwhals._pandas_like.series import PandasLikeSeries
 from narwhals._pandas_like.utils import create_compliant_series
 from narwhals._pandas_like.utils import diagonal_concat
 from narwhals._pandas_like.utils import horizontal_concat
-from narwhals._pandas_like.utils import rename
 from narwhals._pandas_like.utils import vertical_concat
 from narwhals.typing import CompliantNamespace
 from narwhals.utils import import_dtypes_module
@@ -267,18 +266,13 @@ class PandasLikeNamespace(CompliantNamespace[PandasLikeSeries]):
 
             return [
                 PandasLikeSeries(
-                    native_series=rename(
-                        self.concat(
-                            (s.to_frame() for s in series), how="horizontal"
-                        )._native_frame.min(axis=1),
-                        series[0].name,
-                        implementation=self._implementation,
-                        backend_version=self._backend_version,
-                    ),
+                    self.concat(
+                        (s.to_frame() for s in series), how="horizontal"
+                    )._native_frame.min(axis=1),
                     implementation=self._implementation,
                     backend_version=self._backend_version,
                     version=self._version,
-                )
+                ).alias(series[0].name)
             ]
 
         return self._create_expr_from_callable(
@@ -298,18 +292,13 @@ class PandasLikeNamespace(CompliantNamespace[PandasLikeSeries]):
 
             return [
                 PandasLikeSeries(
-                    rename(
-                        self.concat(
-                            (s.to_frame() for s in series), how="horizontal"
-                        )._native_frame.max(axis=1),
-                        series[0].name,
-                        implementation=self._implementation,
-                        backend_version=self._backend_version,
-                    ),
+                    self.concat(
+                        (s.to_frame() for s in series), how="horizontal"
+                    )._native_frame.max(axis=1),
                     implementation=self._implementation,
                     backend_version=self._backend_version,
                     version=self._version,
-                )
+                ).alias(series[0].name)
             ]
 
         return self._create_expr_from_callable(
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 71969444..c7702fb8 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -291,14 +291,7 @@ class PandasLikeSeries(CompliantSeries):
             res = ser.ge(lower_bound) & ser.le(upper_bound)
         else:  # pragma: no cover
             raise AssertionError
-        return self._from_native_series(
-            rename(
-                res,
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+        return self._from_native_series(res).alias(ser.name)
 
     def is_in(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
@@ -329,278 +322,151 @@ class PandasLikeSeries(CompliantSeries):
             ser, other = broadcast_align_and_extract_native(self, other)
         else:
             ser = self._native_series
-        return self._from_native_series(
-            rename(
-                ser.loc[other],
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+        return self._from_native_series(ser.loc[other]).alias(ser.name)
 
     def __eq__(self, other: object) -> PandasLikeSeries:  # type: ignore[override]
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__eq__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__eq__(other),
+        ).alias(ser.name)
 
     def __ne__(self, other: object) -> PandasLikeSeries:  # type: ignore[override]
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__ne__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__ne__(other),
+        ).alias(ser.name)
 
     def __ge__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__ge__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__ge__(other),
+        ).alias(ser.name)
 
     def __gt__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__gt__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__gt__(other),
+        ).alias(ser.name)
 
     def __le__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__le__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__le__(other),
+        ).alias(ser.name)
 
     def __lt__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__lt__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__lt__(other),
+        ).alias(ser.name)
 
     def __and__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__and__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__and__(other),
+        ).alias(ser.name)
 
     def __rand__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__and__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__and__(other),
+        ).alias(ser.name)
 
     def __or__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__or__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__or__(other),
+        ).alias(ser.name)
 
     def __ror__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__or__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__or__(other),
+        ).alias(ser.name)
 
     def __add__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__add__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__add__(other),
+        ).alias(ser.name)
 
     def __radd__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__radd__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__radd__(other),
+        ).alias(ser.name)
 
     def __sub__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__sub__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__sub__(other),
+        ).alias(ser.name)
 
     def __rsub__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__rsub__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__rsub__(other),
+        ).alias(ser.name)
 
     def __mul__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__mul__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__mul__(other),
+        ).alias(ser.name)
 
     def __rmul__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__rmul__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__rmul__(other),
+        ).alias(ser.name)
 
     def __truediv__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__truediv__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__truediv__(other),
+        ).alias(ser.name)
 
     def __rtruediv__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__rtruediv__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__rtruediv__(other),
+        ).alias(ser.name)
 
     def __floordiv__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__floordiv__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__floordiv__(other),
+        ).alias(ser.name)
 
     def __rfloordiv__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__rfloordiv__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__rfloordiv__(other),
+        ).alias(ser.name)
 
     def __pow__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__pow__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__pow__(other),
+        ).alias(ser.name)
 
     def __rpow__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__rpow__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__rpow__(other),
+        ).alias(ser.name)
 
     def __mod__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__mod__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__mod__(other),
+        ).alias(ser.name)
 
     def __rmod__(self, other: Any) -> PandasLikeSeries:
         ser, other = broadcast_align_and_extract_native(self, other)
         return self._from_native_series(
-            rename(
-                ser.__rmod__(other),
-                ser.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.__rmod__(other),
+        ).alias(ser.name)
 
     # Unary
 
@@ -772,10 +638,10 @@ class PandasLikeSeries(CompliantSeries):
             }
         )
         result = self._from_native_series(
-            self._native_series.to_frame()
-            .merge(other, on=self.name, how="left")[tmp_name]
-            .rename(self.name)
-        )
+            self._native_series.to_frame().merge(other, on=self.name, how="left")[
+                tmp_name
+            ]
+        ).alias(self.name)
         if result.is_null().sum() != self.is_null().sum():
             msg = (
                 "replace_strict did not replace all non-null values.\n\n"
@@ -790,13 +656,8 @@ class PandasLikeSeries(CompliantSeries):
         ser = self._native_series
         na_position = "last" if nulls_last else "first"
         return self._from_native_series(
-            rename(
-                ser.sort_values(ascending=not descending, na_position=na_position),
-                self.name,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
-        )
+            ser.sort_values(ascending=not descending, na_position=na_position)
+        ).alias(self.name)
 
     def alias(self, name: str) -> Self:
         if name != self.name:
@@ -872,46 +733,25 @@ class PandasLikeSeries(CompliantSeries):
     # --- descriptive ---
     def is_duplicated(self: Self) -> Self:
         res = self._native_series.duplicated(keep=False)
-        res = rename(
-            res,
-            self.name,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-        )
-        return self._from_native_series(res)
+        return self._from_native_series(res).alias(self.name)
 
     def is_empty(self: Self) -> bool:
         return self._native_series.empty  # type: ignore[no-any-return]
 
     def is_unique(self: Self) -> Self:
-        res = rename(
-            ~self._native_series.duplicated(keep=False),
-            self.name,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-        )
-        return self._from_native_series(res)
+        res = ~self._native_series.duplicated(keep=False)
+        return self._from_native_series(res).alias(self.name)
 
     def null_count(self: Self) -> int:
         return self._native_series.isna().sum()  # type: ignore[no-any-return]
 
     def is_first_distinct(self: Self) -> Self:
-        res = rename(
-            ~self._native_series.duplicated(keep="first"),
-            self.name,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-        )
-        return self._from_native_series(res)
+        res = ~self._native_series.duplicated(keep="first")
+        return self._from_native_series(res).alias(self.name)
 
     def is_last_distinct(self: Self) -> Self:
-        res = rename(
-            ~self._native_series.duplicated(keep="last"),
-            self.name,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-        )
-        return self._from_native_series(res)
+        res = ~self._native_series.duplicated(keep="last")
+        return self._from_native_series(res).alias(self.name)
 
     def is_sorted(self: Self, *, descending: bool = False) -> bool:
         if not isinstance(descending, bool):
diff --git a/narwhals/_pandas_like/series_list.py b/narwhals/_pandas_like/series_list.py
index 9d3b136b..b8a2d20b 100644
--- a/narwhals/_pandas_like/series_list.py
+++ b/narwhals/_pandas_like/series_list.py
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
 
 from narwhals._pandas_like.utils import get_dtype_backend
 from narwhals._pandas_like.utils import narwhals_to_native_dtype
-from narwhals._pandas_like.utils import rename
 from narwhals._pandas_like.utils import set_index
 from narwhals.utils import Implementation
 from narwhals.utils import import_dtypes_module
@@ -28,12 +27,7 @@ class PandasLikeSeriesListNamespace:
             and self._compliant_series._backend_version < (3, 0)
         ):  # pragma: no cover
             native_result = set_index(
-                rename(
-                    native_result,
-                    native_series.name,
-                    implementation=self._compliant_series._implementation,
-                    backend_version=self._compliant_series._backend_version,
-                ),
+                native_result,
                 index=native_series.index,
                 implementation=self._compliant_series._implementation,
                 backend_version=self._compliant_series._backend_version,
@@ -50,4 +44,6 @@ class PandasLikeSeriesListNamespace:
             backend_version=self._compliant_series._backend_version,
             version=self._compliant_series._version,
         )
-        return self._compliant_series._from_native_series(native_result.astype(dtype))
+        return self._compliant_series._from_native_series(
+            native_result.astype(dtype)
+        ).alias(native_series.name)
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 6c813127..1946f97e 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -11,7 +11,6 @@ from typing import Sequence
 from typing import TypeVar
 
 from narwhals.exceptions import ColumnNotFoundError
-from narwhals.exceptions import ShapeError
 from narwhals.utils import Implementation
 from narwhals.utils import import_dtypes_module
 from narwhals.utils import isinstance_or_issubclass
@@ -147,9 +146,7 @@ def broadcast_align_and_extract_native(
     return lhs._native_series, rhs
 
 
-def validate_dataframe_comparand(
-    index: Any, other: Any, *, allow_broadcast: bool, method_name: str
-) -> Any:
+def broadcast_and_extract_dataframe_comparand(index: Any, other: Any) -> Any:
     """Validate RHS of binary operation.
 
     If the comparison isn't supported, return `NotImplemented` so that the
@@ -161,27 +158,13 @@ def validate_dataframe_comparand(
     if isinstance(other, PandasLikeDataFrame):
         return NotImplemented
     if isinstance(other, PandasLikeSeries):
-        len_index = len(index)
         len_other = other.len()
 
         if len_other == 1:
-            if len_index > 1 and not allow_broadcast:
-                msg = (
-                    f"{method_name}'s length: 1 differs from that of the series: "
-                    f"{len_index}"
-                )
-                raise ShapeError(msg)
             # broadcast
             s = other._native_series
             return s.__class__(s.iloc[0], index=index, dtype=s.dtype, name=s.name)
 
-        if len_index != len_other:
-            msg = (
-                f"{method_name}'s length: {len_other} differs from that of the series: "
-                f"{len_index}"
-            )
-            raise ShapeError(msg)
-
         if other._native_series.index is not index:
             return set_index(
                 other._native_series,
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 385a5de0..28f21827 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -18,6 +18,7 @@ from narwhals.dependencies import get_polars
 from narwhals.dependencies import is_numpy_array
 from narwhals.exceptions import LengthChangingExprError
 from narwhals.exceptions import OrderDependentExprError
+from narwhals.exceptions import ShapeError
 from narwhals.schema import Schema
 from narwhals.translate import to_native
 from narwhals.utils import find_stacklevel
@@ -139,14 +140,19 @@ class BaseFrame(Generic[FrameT]):
     def filter(
         self, *predicates: IntoExpr | Iterable[IntoExpr] | list[bool], **constraints: Any
     ) -> Self:
+        flat_predicates = flatten(predicates)
+        if any(
+            getattr(x, "_aggregates", False) or getattr(x, "_changes_length", False)
+            for x in flat_predicates
+        ):
+            msg = "Expressions which aggregate or change length cannot be passed to `filter`."
+            raise ShapeError(msg)
         if not (
             len(predicates) == 1
             and isinstance(predicates[0], list)
             and all(isinstance(x, bool) for x in predicates[0])
         ):
-            predicates, constraints = self._flatten_and_extract(
-                *predicates, **constraints
-            )
+            predicates = [self._extract_compliant(v) for v in flat_predicates]  # type: ignore[assignment]
         return self._from_compliant_dataframe(
             self._compliant_frame.filter(*predicates, **constraints),
         )
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 1845edf3..a6e370f2 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -20,6 +20,7 @@ from narwhals._pandas_like.utils import broadcast_align_and_extract_native
 from narwhals.dataframe import DataFrame
 from narwhals.dataframe import LazyFrame
 from narwhals.dependencies import is_numpy_array
+from narwhals.exceptions import ShapeError
 from narwhals.expr import Expr
 from narwhals.translate import from_native
 from narwhals.utils import Implementation
@@ -2101,6 +2102,12 @@ class When:
         if not self._predicates:
             msg = "At least one predicate needs to be provided to `narwhals.when`."
             raise TypeError(msg)
+        if any(
+            getattr(x, "_aggregates", False) or getattr(x, "_changes_length", False)
+            for x in self._predicates
+        ):
+            msg = "Expressions which aggregate or change length cannot be passed to `filter`."
+            raise ShapeError(msg)
 
     def _extract_predicates(self, plx: Any) -> Any:
         return [extract_compliant(plx, v) for v in self._predicates]


Test patch:
diff --git a/tests/expr_and_series/mean_horizontal_test.py b/tests/expr_and_series/mean_horizontal_test.py
index c1652c83..485bf175 100644
--- a/tests/expr_and_series/mean_horizontal_test.py
+++ b/tests/expr_and_series/mean_horizontal_test.py
@@ -10,11 +10,7 @@ from tests.utils import assert_equal_data
 
 
 @pytest.mark.parametrize("col_expr", [nw.col("a"), "a"])
-def test_meanh(
-    constructor: Constructor, col_expr: Any, request: pytest.FixtureRequest
-) -> None:
-    if "duckdb" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_meanh(constructor: Constructor, col_expr: Any) -> None:
     data = {"a": [1, 3, None, None], "b": [4, None, 6, None]}
     df = nw.from_native(constructor(data))
     result = df.select(horizontal_mean=nw.mean_horizontal(col_expr, nw.col("b")))
@@ -22,9 +18,7 @@ def test_meanh(
     assert_equal_data(result, expected)
 
 
-def test_meanh_all(constructor: Constructor, request: pytest.FixtureRequest) -> None:
-    if "duckdb" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_meanh_all(constructor: Constructor) -> None:
     data = {"a": [2, 4, 6], "b": [10, 20, 30]}
     df = nw.from_native(constructor(data))
     result = df.select(nw.mean_horizontal(nw.all()))
diff --git a/tests/expr_and_series/when_test.py b/tests/expr_and_series/when_test.py
index 140626e4..6c509a40 100644
--- a/tests/expr_and_series/when_test.py
+++ b/tests/expr_and_series/when_test.py
@@ -4,6 +4,7 @@ import numpy as np
 import pytest
 
 import narwhals.stable.v1 as nw
+from narwhals.exceptions import ShapeError
 from tests.utils import Constructor
 from tests.utils import ConstructorEager
 from tests.utils import assert_equal_data
@@ -133,6 +134,12 @@ def test_when_then_otherwise_into_expr(constructor: Constructor) -> None:
     assert_equal_data(result, expected)
 
 
+def test_when_then_invalid(constructor: Constructor) -> None:
+    df = nw.from_native(constructor(data))
+    with pytest.raises(ShapeError):
+        df.select(nw.when(nw.col("a").sum() > 1).then("c"))
+
+
 def test_when_then_otherwise_lit_str(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") > 1).then(nw.col("b")).otherwise(nw.lit("z")))
diff --git a/tests/frame/filter_test.py b/tests/frame/filter_test.py
index 96aa2b44..cbfd22b5 100644
--- a/tests/frame/filter_test.py
+++ b/tests/frame/filter_test.py
@@ -37,18 +37,7 @@ def test_filter_with_boolean_list(constructor: Constructor) -> None:
 def test_filter_raise_on_agg_predicate(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]}
     df = nw.from_native(constructor(data))
-
-    context = (
-        pytest.raises(
-            ShapeError,
-            match="filter's length: 1 differs from that of the series: 3",
-        )
-        if any(x in str(constructor) for x in ("pandas", "pyarrow", "modin"))
-        else does_not_raise()
-        if "polars" in str(constructor)
-        else pytest.raises(Exception)  # type: ignore[arg-type] # noqa: PT011
-    )
-    with context:
+    with pytest.raises(ShapeError):
         df.filter(nw.col("a").max() > 2).lazy().collect()