Repository: narwhals-dev/narwhals

Issue:
[Enh]: TODO: ArrowSeriesCatNamespace has an operation that is potentially expensive
### Please describe the purpose of the new feature or describe the problem to solve.

See [this snippet](https://github.com/narwhals-dev/narwhals/blob/c904b102bd95dec026ce756cecfda15dabe71f41/narwhals/_arrow/series.py#L179-L190)

```python
class ArrowSeriesCatNamespace:
    def __init__(self, series: ArrowSeries) -> None:
        self._arrow_series = series

    def get_categories(self) -> ArrowSeries:
        pa = get_pyarrow()
        ca = self._arrow_series._native_series
        # TODO(Unassigned): this looks potentially expensive - is there no better way?
        out = pa.chunked_array(
            [pa.concat_arrays([x.dictionary for x in ca.chunks]).unique()]
        )
        return self._arrow_series._from_native_series(out)
```

Discussion:
FBruzzesi: Unrelated CI failure

Patch:
diff --git a/docs/how_it_works.md b/docs/how_it_works.md
index 70bc54bf..6a670358 100644
--- a/docs/how_it_works.md
+++ b/docs/how_it_works.md
@@ -266,7 +266,6 @@ In order to tell whether an aggregation is simple, Narwhals uses the private `_d
 ```python exec="1" result="python" session="pandas_impl" source="above"
 print(pn.col("a").mean())
 print((pn.col("a") + 1).mean())
-print(pn.mean("a"))
 ```
 
 For simple aggregations, Narwhals can just look at `_depth` and `function_name` and figure out
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 16669719..8576dd2b 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -641,7 +641,7 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
     def unique(
         self: Self,
-        subset: str | list[str] | None,
+        subset: list[str] | None,
         *,
         keep: Literal["any", "first", "last", "none"],
         maintain_order: bool,
@@ -653,9 +653,6 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
         import pyarrow.compute as pc
 
         df = self._native_frame
-
-        if isinstance(subset, str):
-            subset = [subset]
         subset = subset or self.columns
 
         if keep in {"any", "first", "last"}:
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index 884bc8f0..ea37ae76 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -348,31 +348,6 @@ class ArrowNamespace(CompliantNamespace[ArrowSeries]):
             result_table, backend_version=self._backend_version, version=self._version
         )
 
-    def sum(self: Self, *column_names: str) -> ArrowExpr:
-        return ArrowExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).sum()
-
-    def mean(self: Self, *column_names: str) -> ArrowExpr:
-        return ArrowExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).mean()
-
-    def median(self: Self, *column_names: str) -> ArrowExpr:
-        return ArrowExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).median()
-
-    def max(self: Self, *column_names: str) -> ArrowExpr:
-        return ArrowExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).max()
-
-    def min(self: Self, *column_names: str) -> ArrowExpr:
-        return ArrowExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).min()
-
     @property
     def selectors(self: Self) -> ArrowSelectorNamespace:
         return ArrowSelectorNamespace(
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 2f29ab9d..239ab5c1 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -487,6 +487,12 @@ class ArrowSeries(CompliantSeries):
         import pyarrow.compute as pc
 
         ser = self._native_series
+        _, lower_bound = broadcast_and_extract_native(
+            self, lower_bound, self._backend_version
+        )
+        _, upper_bound = broadcast_and_extract_native(
+            self, upper_bound, self._backend_version
+        )
         if closed == "left":
             ge = pc.greater_equal(ser, lower_bound)
             lt = pc.less(ser, upper_bound)
@@ -1413,10 +1419,8 @@ class ArrowSeriesCatNamespace:
         import pyarrow as pa
 
         ca = self._compliant_series._native_series
-        # TODO(Unassigned): this looks potentially expensive - is there no better way?
-        # https://github.com/narwhals-dev/narwhals/issues/464
         out = pa.chunked_array(
-            [pa.concat_arrays([x.dictionary for x in ca.chunks]).unique()]
+            [pa.concat_arrays(x.dictionary for x in ca.chunks).unique()]
         )
         return self._compliant_series._from_native_series(out)
 
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index 8c895b81..f284ce42 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -530,7 +530,7 @@ def pad_series(
         pad_left = pa.array([None] * offset_left, type=native_series.type)
         pad_right = pa.array([None] * offset_right, type=native_series.type)
         padded_arr = series._from_native_series(
-            pa.concat_arrays([pad_left, native_series.combine_chunks(), pad_right])
+            pa.concat_arrays([pad_left, *native_series.chunks, pad_right])
         )
         offset = offset_left + offset_right
     else:
diff --git a/narwhals/_dask/dataframe.py b/narwhals/_dask/dataframe.py
index 0e762b1f..0edf4121 100644
--- a/narwhals/_dask/dataframe.py
+++ b/narwhals/_dask/dataframe.py
@@ -198,14 +198,13 @@ class DaskLazyFrame(CompliantLazyFrame):
 
     def unique(
         self: Self,
-        subset: str | list[str] | None,
+        subset: list[str] | None,
         *,
         keep: Literal["any", "first", "last", "none"] = "any",
         maintain_order: bool = False,
     ) -> Self:
         # The param `maintain_order` is only here for compatibility with the Polars API
         # and has no effect on the output.
-        subset = flatten(subset) if subset else None
         native_frame = self._native_frame
         if keep == "none":
             subset = subset or self.columns
diff --git a/narwhals/_dask/namespace.py b/narwhals/_dask/namespace.py
index 38cd16a8..126afaae 100644
--- a/narwhals/_dask/namespace.py
+++ b/narwhals/_dask/namespace.py
@@ -91,31 +91,6 @@ class DaskNamespace(CompliantNamespace["dask_expr.Series"]):
             kwargs={},
         )
 
-    def min(self, *column_names: str) -> DaskExpr:
-        return DaskExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).min()
-
-    def max(self, *column_names: str) -> DaskExpr:
-        return DaskExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).max()
-
-    def mean(self, *column_names: str) -> DaskExpr:
-        return DaskExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).mean()
-
-    def median(self, *column_names: str) -> DaskExpr:
-        return DaskExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).median()
-
-    def sum(self, *column_names: str) -> DaskExpr:
-        return DaskExpr.from_column_names(
-            *column_names, backend_version=self._backend_version, version=self._version
-        ).sum()
-
     def len(self) -> DaskExpr:
         import dask.dataframe as dd
         import pandas as pd
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 6f8706ae..c10aacec 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -684,7 +684,7 @@ class PandasLikeDataFrame:
 
     def unique(
         self: Self,
-        subset: str | list[str] | None,
+        subset: list[str] | None,
         *,
         keep: Literal["any", "first", "last", "none"] = "any",
         maintain_order: bool = False,
@@ -692,7 +692,6 @@ class PandasLikeDataFrame:
         # The param `maintain_order` is only here for compatibility with the Polars API
         # and has no effect on the output.
         mapped_keep = {"none": False, "any": "first"}.get(keep, keep)
-        subset = flatten(subset) if subset else None
         return self._from_native_frame(
             self._native_frame.drop_duplicates(subset=subset, keep=mapped_keep)
         )
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index 0b060708..7885d7de 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -169,47 +169,6 @@ class PandasLikeNamespace(CompliantNamespace[PandasLikeSeries]):
             kwargs={},
         )
 
-    # --- reduction ---
-    def sum(self, *column_names: str) -> PandasLikeExpr:
-        return PandasLikeExpr.from_column_names(
-            *column_names,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-            version=self._version,
-        ).sum()
-
-    def mean(self, *column_names: str) -> PandasLikeExpr:
-        return PandasLikeExpr.from_column_names(
-            *column_names,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-            version=self._version,
-        ).mean()
-
-    def median(self, *column_names: str) -> PandasLikeExpr:
-        return PandasLikeExpr.from_column_names(
-            *column_names,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-            version=self._version,
-        ).median()
-
-    def max(self, *column_names: str) -> PandasLikeExpr:
-        return PandasLikeExpr.from_column_names(
-            *column_names,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-            version=self._version,
-        ).max()
-
-    def min(self, *column_names: str) -> PandasLikeExpr:
-        return PandasLikeExpr.from_column_names(
-            *column_names,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-            version=self._version,
-        ).min()
-
     def len(self) -> PandasLikeExpr:
         return PandasLikeExpr(
             lambda df: [
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 1d895e14..c9f3d006 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -263,6 +263,8 @@ class PandasLikeSeries(CompliantSeries):
         self, lower_bound: Any, upper_bound: Any, closed: str = "both"
     ) -> PandasLikeSeries:
         ser = self._native_series
+        _, lower_bound = broadcast_align_and_extract_native(self, lower_bound)
+        _, upper_bound = broadcast_align_and_extract_native(self, upper_bound)
         if closed == "left":
             res = ser.ge(lower_bound) & ser.lt(upper_bound)
         elif closed == "right":
@@ -273,7 +275,14 @@ class PandasLikeSeries(CompliantSeries):
             res = ser.ge(lower_bound) & ser.le(upper_bound)
         else:  # pragma: no cover
             raise AssertionError
-        return self._from_native_series(res)
+        return self._from_native_series(
+            rename(
+                res,
+                ser.name,
+                implementation=self._implementation,
+                backend_version=self._backend_version,
+            )
+        )
 
     def is_in(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
diff --git a/narwhals/_polars/namespace.py b/narwhals/_polars/namespace.py
index 3e1ea176..00e005c3 100644
--- a/narwhals/_polars/namespace.py
+++ b/narwhals/_polars/namespace.py
@@ -128,17 +128,6 @@ class PolarsNamespace:
             pl.lit(value), version=self._version, backend_version=self._backend_version
         )
 
-    def mean(self: Self, *column_names: str) -> PolarsExpr:
-        import polars as pl
-
-        from narwhals._polars.expr import PolarsExpr
-
-        return PolarsExpr(
-            pl.mean([*column_names]),  # type: ignore[arg-type]
-            version=self._version,
-            backend_version=self._backend_version,
-        )
-
     def mean_horizontal(self: Self, *exprs: IntoPolarsExpr) -> PolarsExpr:
         import polars as pl
 
@@ -160,17 +149,6 @@ class PolarsNamespace:
             backend_version=self._backend_version,
         )
 
-    def median(self: Self, *column_names: str) -> PolarsExpr:
-        import polars as pl
-
-        from narwhals._polars.expr import PolarsExpr
-
-        return PolarsExpr(
-            pl.median([*column_names]),  # type: ignore[arg-type]
-            version=self._version,
-            backend_version=self._backend_version,
-        )
-
     def concat_str(
         self,
         exprs: Iterable[IntoPolarsExpr],
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 2b59dd95..ccdba365 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -153,6 +153,8 @@ class BaseFrame(Generic[FrameT]):
         keep: Literal["any", "first", "last", "none"] = "any",
         maintain_order: bool = False,
     ) -> Self:
+        if isinstance(subset, str):
+            subset = [subset]
         return self._from_compliant_dataframe(
             self._compliant_frame.unique(
                 subset=subset, keep=keep, maintain_order=maintain_order
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 777cae61..ba0896db 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -1666,7 +1666,10 @@ class Expr:
 
     # --- transform ---
     def is_between(
-        self, lower_bound: Any, upper_bound: Any, closed: str = "both"
+        self,
+        lower_bound: Any | IntoExpr,
+        upper_bound: Any | IntoExpr,
+        closed: str = "both",
     ) -> Self:
         """Check if this expression is between the given lower and upper bounds.
 
@@ -1724,7 +1727,9 @@ class Expr:
         """
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).is_between(
-                lower_bound, upper_bound, closed
+                extract_compliant(plx, lower_bound),
+                extract_compliant(plx, upper_bound),
+                closed,
             )
         )
 
@@ -6049,7 +6054,7 @@ def col(*names: str | Iterable[str]) -> Expr:
     """Creates an expression that references one or more columns by their name(s).
 
     Arguments:
-        names: Name(s) of the columns to use in the aggregation function.
+        names: Name(s) of the columns to use.
 
     Returns:
         A new expression.
@@ -6308,7 +6313,7 @@ def sum(*columns: str) -> Expr:
         ----
         a: [[3]]
     """
-    return Expr(lambda plx: plx.sum(*columns))
+    return Expr(lambda plx: plx.col(*columns).sum())
 
 
 def mean(*columns: str) -> Expr:
@@ -6359,7 +6364,7 @@ def mean(*columns: str) -> Expr:
         ----
         a: [[4]]
     """
-    return Expr(lambda plx: plx.mean(*columns))
+    return Expr(lambda plx: plx.col(*columns).mean())
 
 
 def median(*columns: str) -> Expr:
@@ -6411,7 +6416,7 @@ def median(*columns: str) -> Expr:
         ----
         a: [[4]]
     """
-    return Expr(lambda plx: plx.median(*columns))
+    return Expr(lambda plx: plx.col(*columns).median())
 
 
 def min(*columns: str) -> Expr:
@@ -6462,7 +6467,7 @@ def min(*columns: str) -> Expr:
         ----
         b: [[5]]
     """
-    return Expr(lambda plx: plx.min(*columns))
+    return Expr(lambda plx: plx.col(*columns).min())
 
 
 def max(*columns: str) -> Expr:
@@ -6513,7 +6518,7 @@ def max(*columns: str) -> Expr:
         ----
         a: [[2]]
     """
-    return Expr(lambda plx: plx.max(*columns))
+    return Expr(lambda plx: plx.col(*columns).max())
 
 
 def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
diff --git a/narwhals/series.py b/narwhals/series.py
index 228fb01c..8f15ff0c 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -256,6 +256,9 @@ class Series(Generic[IntoSeriesT]):
             indices: Position(s) to set items at.
             values: Values to set.
 
+        Returns:
+            A new Series with values set at given positions.
+
         Note:
             This method always returns a new Series, without modifying the original one.
             Using this function in a for-loop is an anti-pattern, we recommend building
@@ -319,6 +322,9 @@ class Series(Generic[IntoSeriesT]):
     def shape(self) -> tuple[int]:
         """Get the shape of the Series.
 
+        Returns:
+            A tuple containing the length of the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -359,6 +365,9 @@ class Series(Generic[IntoSeriesT]):
     def pipe(self, function: Callable[[Any], Self], *args: Any, **kwargs: Any) -> Self:
         """Pipe function call.
 
+        Returns:
+            A new Series with the results of the piped function applied.
+
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
@@ -416,6 +425,9 @@ class Series(Generic[IntoSeriesT]):
 
         Null values count towards the total.
 
+        Returns:
+            The number of elements in the Series.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeries
@@ -444,6 +456,9 @@ class Series(Generic[IntoSeriesT]):
     def dtype(self: Self) -> DType:
         """Get the data type of the Series.
 
+        Returns:
+            The data type of the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -472,6 +487,9 @@ class Series(Generic[IntoSeriesT]):
     def name(self) -> str:
         """Get the name of the Series.
 
+        Returns:
+            The name of the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -639,7 +657,7 @@ class Series(Generic[IntoSeriesT]):
         """Convert to dataframe.
 
         Returns:
-            A new DataFrame.
+            A DataFrame containing this Series as a single column.
 
         Examples:
             >>> import pandas as pd
@@ -719,6 +737,9 @@ class Series(Generic[IntoSeriesT]):
     def mean(self) -> Any:
         """Reduce this Series to the mean value.
 
+        Returns:
+            The average of all elements in the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -749,6 +770,9 @@ class Series(Generic[IntoSeriesT]):
         Notes:
             Results might slightly differ across backends due to differences in the underlying algorithms used to compute the median.
 
+        Returns:
+            The median value of all elements in the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -910,6 +934,9 @@ class Series(Generic[IntoSeriesT]):
     def min(self) -> Any:
         """Get the minimal value in this Series.
 
+        Returns:
+            The minimum value in the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -937,6 +964,9 @@ class Series(Generic[IntoSeriesT]):
     def max(self) -> Any:
         """Get the maximum value in this Series.
 
+        Returns:
+            The maximum value in the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1028,6 +1058,9 @@ class Series(Generic[IntoSeriesT]):
     def sum(self) -> Any:
         """Reduce this Series to the sum value.
 
+        Returns:
+            The sum of all elements in the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1059,6 +1092,9 @@ class Series(Generic[IntoSeriesT]):
             ddof: "Delta Degrees of Freedom": the divisor used in the calculation is N - ddof,
                      where N represents the number of elements.
 
+        Returns:
+            The standard deviation of all elements in the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1224,6 +1260,9 @@ class Series(Generic[IntoSeriesT]):
         Arguments:
             other: Sequence of primitive type.
 
+        Returns:
+            A new Series with boolean values indicating if the elements are in the other sequence.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1303,6 +1342,9 @@ class Series(Generic[IntoSeriesT]):
             See [null_handling](../pandas_like_concepts/null_handling.md/)
             for reference.
 
+        Returns:
+            A new Series with null values removed.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1395,7 +1437,7 @@ class Series(Generic[IntoSeriesT]):
             reverse: reverse the operation
 
         Returns:
-            A new Series with the cumulative sum of the values.
+            A new Series with the cumulative sum of non-null values.
 
         Examples:
             >>> import pandas as pd
@@ -1440,6 +1482,9 @@ class Series(Generic[IntoSeriesT]):
                 expensive to compute. Settings this to `True` blocks the possibility
                 to run on the streaming engine for Polars.
 
+        Returns:
+            A new Series with duplicate values removed.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1487,6 +1532,9 @@ class Series(Generic[IntoSeriesT]):
 
                 s.diff().fill_null(0).cast(nw.Int64)
 
+        Returns:
+            A new Series with the difference between each element and its predecessor.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1527,6 +1575,9 @@ class Series(Generic[IntoSeriesT]):
             n: Number of indices to shift forward. If a negative value is passed,
                 values are shifted in the opposite direction instead.
 
+        Returns:
+            A new Series with values shifted by n positions.
+
         Notes:
             pandas may change the dtype here, for example when introducing missing
             values in an integer column. To ensure, that the dtype doesn't change,
@@ -1586,6 +1637,9 @@ class Series(Generic[IntoSeriesT]):
             seed: Seed for the random number generator. If set to None (default), a random
                 seed is generated for each sample operation.
 
+        Returns:
+            A new Series containing randomly sampled values from the original Series.
+
         Notes:
             The `sample` method returns a Series with a specified number of
             randomly selected items chosen from this Series.
@@ -1729,6 +1783,9 @@ class Series(Generic[IntoSeriesT]):
         Arguments:
             name: The new name.
 
+        Returns:
+            A new Series with the updated name.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1793,6 +1850,9 @@ class Series(Generic[IntoSeriesT]):
                 (default), the data type is determined automatically based on the other
                 inputs.
 
+        Returns:
+            A new Series with values replaced according to the mapping.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeriesT
@@ -1858,6 +1918,9 @@ class Series(Generic[IntoSeriesT]):
             descending: Sort in descending order.
             nulls_last: Place null values last instead of first.
 
+        Returns:
+            A new sorted Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1922,6 +1985,9 @@ class Series(Generic[IntoSeriesT]):
             See [null_handling](../pandas_like_concepts/null_handling.md/)
             for reference.
 
+        Returns:
+            A boolean Series indicating which values are null.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1984,6 +2050,9 @@ class Series(Generic[IntoSeriesT]):
             See [null_handling](../pandas_like_concepts/null_handling.md/)
             for reference.
 
+        Returns:
+            A new Series with null values filled according to the specified value or strategy.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -2069,21 +2138,22 @@ class Series(Generic[IntoSeriesT]):
         )
 
     def is_between(
-        self, lower_bound: Any, upper_bound: Any, closed: str = "both"
+        self, lower_bound: Any | Self, upper_bound: Any | Self, closed: str = "both"
     ) -> Self:
         """Get a boolean mask of the values that are between the given lower/upper bounds.
 
         Arguments:
             lower_bound: Lower bound value.
-
             upper_bound: Upper bound value.
-
             closed: Define which sides of the interval are closed (inclusive).
 
         Notes:
             If the value of the `lower_bound` is greater than that of the `upper_bound`,
             then the values will be False, as no value can satisfy the condition.
 
+        Returns:
+            A boolean Series indicating which values are between the given bounds.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -2119,12 +2189,19 @@ class Series(Generic[IntoSeriesT]):
             ]
         """
         return self._from_compliant_series(
-            self._compliant_series.is_between(lower_bound, upper_bound, closed=closed)
+            self._compliant_series.is_between(
+                self._extract_native(lower_bound),
+                self._extract_native(upper_bound),
+                closed=closed,
+            )
         )
 
     def n_unique(self) -> int:
         """Count the number of unique values.
 
+        Returns:
+            Number of unique values in the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -2152,6 +2229,9 @@ class Series(Generic[IntoSeriesT]):
     def to_numpy(self) -> np.ndarray:
         """Convert to numpy.
 
+        Returns:
+            NumPy ndarray representation of the Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -2180,6 +2260,9 @@ class Series(Generic[IntoSeriesT]):
     def to_pandas(self) -> pd.Series:
         """Convert to pandas.
 
+        Returns:
+            A pandas Series containing the data from this Series.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -2337,6 +2420,9 @@ class Series(Generic[IntoSeriesT]):
     def filter(self, other: Any) -> Self:
         """Filter elements in the Series based on a condition.
 
+        Returns:
+            A new Series with elements that satisfy the condition.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -2377,7 +2463,7 @@ class Series(Generic[IntoSeriesT]):
         r"""Get a mask of all duplicated rows in the Series.
 
         Returns:
-            A new Series.
+            A new Series with boolean values indicating duplicated rows.
 
         Examples:
             >>> import narwhals as nw
@@ -2416,6 +2502,9 @@ class Series(Generic[IntoSeriesT]):
     def is_empty(self: Self) -> bool:
         r"""Check if the series is empty.
 
+        Returns:
+            A boolean indicating if the series is empty.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeries
@@ -2446,6 +2535,9 @@ class Series(Generic[IntoSeriesT]):
     def is_unique(self: Self) -> Self:
         r"""Get a mask of all unique rows in the Series.
 
+        Returns:
+            A new Series with boolean values indicating unique rows.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeriesT
@@ -2489,6 +2581,9 @@ class Series(Generic[IntoSeriesT]):
             See [null_handling](../pandas_like_concepts/null_handling.md/)
             for reference.
 
+        Returns:
+            The number of null values in the Series.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeries
@@ -2521,6 +2616,9 @@ class Series(Generic[IntoSeriesT]):
     def is_first_distinct(self: Self) -> Self:
         r"""Return a boolean mask indicating the first occurrence of each distinct value.
 
+        Returns:
+            A new Series with boolean values indicating the first occurrence of each distinct value.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeriesT
@@ -2561,6 +2659,9 @@ class Series(Generic[IntoSeriesT]):
     def is_last_distinct(self: Self) -> Self:
         r"""Return a boolean mask indicating the last occurrence of each distinct value.
 
+        Returns:
+            A new Series with boolean values indicating the last occurrence of each distinct value.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeriesT
@@ -2604,6 +2705,9 @@ class Series(Generic[IntoSeriesT]):
         Arguments:
             descending: Check if the Series is sorted in descending order.
 
+        Returns:
+            A boolean indicating if the Series is sorted.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeries
@@ -2652,7 +2756,9 @@ class Series(Generic[IntoSeriesT]):
             normalize: If true gives relative frequencies of the unique values
 
         Returns:
-            A new DataFrame.
+            A DataFrame with two columns:
+            - The original values as first column
+            - Either count or proportion as second column, depending on normalize parameter.
 
         Examples:
             >>> import narwhals as nw
@@ -2709,6 +2815,9 @@ class Series(Generic[IntoSeriesT]):
             quantile: Quantile between 0.0 and 1.0.
             interpolation: Interpolation method.
 
+        Returns:
+            The quantile value.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeries
@@ -2749,6 +2858,9 @@ class Series(Generic[IntoSeriesT]):
             mask: Boolean Series
             other: Series of same type.
 
+        Returns:
+            A new Series with values selected from self or other based on the mask.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeriesT
@@ -2805,6 +2917,9 @@ class Series(Generic[IntoSeriesT]):
         If no index is provided, this is equivalent to `s[0]`, with a check
         that the shape is (1,). With an index, this is equivalent to `s[index]`.
 
+        Returns:
+            The scalar value of the Series or the element at the given index.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeries
@@ -2839,6 +2954,9 @@ class Series(Generic[IntoSeriesT]):
         Arguments:
             n: Number of rows to return.
 
+        Returns:
+            A new Series containing the first n characters of each string.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeriesT
@@ -2879,6 +2997,9 @@ class Series(Generic[IntoSeriesT]):
         Arguments:
             n: Number of rows to return.
 
+        Returns:
+            A new Series with the last n rows.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeriesT
@@ -2918,6 +3039,9 @@ class Series(Generic[IntoSeriesT]):
         Arguments:
             decimals: Number of decimals to round by.
 
+        Returns:
+            A new Series with rounded values.
+
         Notes:
             For values exactly halfway between rounded decimal values pandas behaves differently than Polars and Arrow.
 
@@ -2969,6 +3093,9 @@ class Series(Generic[IntoSeriesT]):
             separator: Separator/delimiter used when generating column names.
             drop_first: Remove the first category from the variable being encoded.
 
+        Returns:
+            A new DataFrame containing the dummy/indicator variables.
+
         Notes:
             pandas and Polars handle null values differently. Polars distinguishes
             between NaN and Null, whereas pandas doesn't.
@@ -3039,6 +3166,9 @@ class Series(Generic[IntoSeriesT]):
             n: Gather every *n*-th row.
             offset: Starting index.
 
+        Returns:
+            A new Series with every nth value starting from the offset.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeriesT
@@ -3075,6 +3205,9 @@ class Series(Generic[IntoSeriesT]):
     def to_arrow(self: Self) -> pa.Array:
         r"""Convert to arrow.
 
+        Returns:
+            A PyArrow Array containing the data from the Series.
+
         Examples:
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoSeries
@@ -3116,6 +3249,9 @@ class Series(Generic[IntoSeriesT]):
 
         Can return multiple values.
 
+        Returns:
+            A new Series containing the mode(s) (values that appear most frequently).
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -3837,6 +3973,9 @@ class SeriesCatNamespace(Generic[SeriesT]):
     def get_categories(self: Self) -> SeriesT:
         """Get unique categories from column.
 
+        Returns:
+            A new Series containing the unique categories.
+
         Examples:
             Let's create some series:
 
@@ -3881,6 +4020,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
     def len_chars(self: Self) -> SeriesT:
         r"""Return the length of each string as the number of characters.
 
+        Returns:
+            A new Series containing the length of each string in characters.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -3932,6 +4074,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
             literal: Treat `pattern` as a literal string.
             n: Number of matches to replace.
 
+        Returns:
+            A new Series with the regex/literal pattern replaced with the specified value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -3979,6 +4124,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
             value: String that will replace the matched substring.
             literal: Treat `pattern` as a literal string.
 
+        Returns:
+            A new Series with all occurrences of pattern replaced with the specified value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4022,6 +4170,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
         Arguments:
             characters: The set of characters to be removed. All combinations of this set of characters will be stripped from the start and end of the string. If set to None (default), all leading and trailing whitespace is removed instead.
 
+        Returns:
+            A new Series with leading and trailing characters removed.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4063,6 +4214,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
         Arguments:
             prefix: prefix substring
 
+        Returns:
+            A new Series with boolean values indicating if each string starts with the prefix.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4105,6 +4259,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
         Arguments:
             suffix: suffix substring
 
+        Returns:
+            A new Series with boolean values indicating if each string ends with the suffix.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4149,6 +4306,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
             literal: If True, treats the pattern as a literal string.
                      If False, assumes the pattern is a regular expression.
 
+        Returns:
+            A new Series with boolean values indicating if each string contains the pattern.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4211,6 +4371,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
             length: Length of the slice. If set to `None` (default), the slice is taken to the
                 end of the string.
 
+        Returns:
+            A new Series containing subslices of each string.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4280,6 +4443,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
         Arguments:
             n: Number of elements to take. Negative indexing is supported (see note (1.))
 
+        Returns:
+            A new Series containing the first n characters of each string.
+
         Notes:
             1. When the `n` input is negative, `head` returns characters up to the n-th from the end of the string.
                 For example, if `n = -3`, then all characters except the last three are returned.
@@ -4328,6 +4494,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
         Arguments:
             n: Number of elements to take. Negative indexing is supported (see note (1.))
 
+        Returns:
+            A new Series containing the last n characters of each string.
+
         Notes:
             1. When the `n` input is negative, `tail` returns characters starting from the n-th from the beginning of
                 the string. For example, if `n = -3`, then all characters except the first three are returned.
@@ -4373,6 +4542,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
     def to_uppercase(self) -> SeriesT:
         r"""Transform string to uppercase variant.
 
+        Returns:
+            A new Series with values converted to uppercase.
+
         Notes:
             The PyArrow backend will convert 'ß' to 'ẞ' instead of 'SS'.
             For more info see: https://github.com/apache/arrow/issues/34599
@@ -4423,6 +4595,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
     def to_lowercase(self) -> SeriesT:
         r"""Transform string to lowercase variant.
 
+        Returns:
+            A new Series with values converted to lowercase.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4482,6 +4657,9 @@ class SeriesStringNamespace(Generic[SeriesT]):
             format: Format to use for conversion. If set to None (default), the format is
                 inferred from the data.
 
+        Returns:
+            A new Series with datetime dtype.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4533,6 +4711,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def date(self: Self) -> SeriesT:
         """Get the date in a datetime series.
 
+        Returns:
+            A new Series with the date portion of the datetime values.
+
         Raises:
             NotImplementedError: If pandas default backend is being used.
 
@@ -4574,6 +4755,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def year(self: Self) -> SeriesT:
         """Get the year in a datetime series.
 
+        Returns:
+            A new Series containing the year component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4611,6 +4795,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def month(self: Self) -> SeriesT:
         """Gets the month in a datetime series.
 
+        Returns:
+            A new Series containing the month component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4648,6 +4835,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def day(self: Self) -> SeriesT:
         """Extracts the day in a datetime series.
 
+        Returns:
+            A new Series containing the day component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4685,6 +4875,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def hour(self: Self) -> SeriesT:
         """Extracts the hour in a datetime series.
 
+        Returns:
+            A new Series containing the hour component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4722,6 +4915,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def minute(self: Self) -> SeriesT:
         """Extracts the minute in a datetime series.
 
+        Returns:
+            A new Series containing the minute component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4759,6 +4955,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def second(self: Self) -> SeriesT:
         """Extracts the seconds in a datetime series.
 
+        Returns:
+            A new Series containing the second component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4796,6 +4995,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def millisecond(self: Self) -> SeriesT:
         """Extracts the milliseconds in a datetime series.
 
+        Returns:
+            A new Series containing the millisecond component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4846,6 +5048,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def microsecond(self: Self) -> SeriesT:
         """Extracts the microseconds in a datetime series.
 
+        Returns:
+            A new Series containing the microsecond component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4896,6 +5101,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def nanosecond(self: Self) -> SeriesT:
         """Extract the nanoseconds in a date series.
 
+        Returns:
+            A new Series containing the nanosecond component of each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4936,6 +5144,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def ordinal_day(self: Self) -> SeriesT:
         """Get ordinal day.
 
+        Returns:
+            A new Series containing the ordinal day (day of year) for each datetime value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -4978,6 +5189,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
             however, pandas may change the dtype to float when there are missing values,
             consider using `fill_null()` in this case.
 
+        Returns:
+            A new Series containing the total number of minutes for each timedelta value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -5020,6 +5234,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
             however, pandas may change the dtype to float when there are missing values,
             consider using `fill_null()` in this case.
 
+        Returns:
+            A new Series containing the total number of seconds for each timedelta value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -5062,6 +5279,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
             however, pandas may change the dtype to float when there are missing values,
             consider using `fill_null()` in this case.
 
+        Returns:
+            A new Series containing the total number of milliseconds for each timedelta value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -5102,6 +5322,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def total_microseconds(self: Self) -> SeriesT:
         """Get total microseconds.
 
+        Returns:
+            A new Series containing the total number of microseconds for each timedelta value.
+
         Notes:
             The function outputs the total microseconds in the int dtype by default,
             however, pandas may change the dtype to float when there are missing values,
@@ -5152,6 +5375,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
             however, pandas may change the dtype to float when there are missing values,
             consider using `fill_null()` in this case.
 
+        Returns:
+            A new Series containing the total number of nanoseconds for each timedelta value.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -5189,6 +5415,12 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
     def to_string(self: Self, format: str) -> SeriesT:  # noqa: A002
         """Convert a Date/Time/Datetime series into a String series with the given format.
 
+        Arguments:
+            format: Format string for converting the datetime to string.
+
+        Returns:
+            A new Series with the datetime values formatted as strings according to the specified format.
+
         Notes:
             Unfortunately, different libraries interpret format directives a bit
             differently.
@@ -5267,6 +5499,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Arguments:
             time_zone: Target time zone.
 
+        Returns:
+            A new Series with the specified time zone.
+
         Examples:
             >>> from datetime import datetime, timezone
             >>> import narwhals as nw
@@ -5323,6 +5558,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Arguments:
             time_zone: Target time zone.
 
+        Returns:
+            A new Series with the specified time zone.
+
         Examples:
             >>> from datetime import datetime, timezone
             >>> import narwhals as nw
@@ -5380,6 +5618,9 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
             time_unit: {'ns', 'us', 'ms'}
                 Time unit.
 
+        Returns:
+            A new Series with timestamps in the specified time unit.
+
         Examples:
             >>> from datetime import date
             >>> import narwhals as nw
diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py
index 4bd0cc1b..2b5be2ee 100644
--- a/narwhals/stable/v1/__init__.py
+++ b/narwhals/stable/v1/__init__.py
@@ -478,7 +478,7 @@ class Series(NwSeries[Any]):
         """Convert to dataframe.
 
         Returns:
-            A new DataFrame.
+            A DataFrame containing this Series as a single column.
 
         Examples:
             >>> import pandas as pd
@@ -535,7 +535,9 @@ class Series(NwSeries[Any]):
             normalize: If true gives relative frequencies of the unique values
 
         Returns:
-            A new DataFrame.
+            A DataFrame with two columns:
+            - The original values as first column
+            - Either count or proportion as second column, depending on normalize parameter.
 
         Examples:
             >>> import narwhals as nw
@@ -2319,7 +2321,7 @@ def col(*names: str | Iterable[str]) -> Expr:
     """Creates an expression that references one or more columns by their name(s).
 
     Arguments:
-        names: Name(s) of the columns to use in the aggregation function.
+        names: Name(s) of the columns to use.
 
     Returns:
         A new expression.


Test patch:
diff --git a/tests/expr_and_series/is_between_test.py b/tests/expr_and_series/is_between_test.py
index 8d08c6fa..57ad545c 100644
--- a/tests/expr_and_series/is_between_test.py
+++ b/tests/expr_and_series/is_between_test.py
@@ -7,10 +7,6 @@ from tests.utils import Constructor
 from tests.utils import ConstructorEager
 from tests.utils import assert_equal_data
 
-data = {
-    "a": [1, 4, 2, 5],
-}
-
 
 @pytest.mark.parametrize(
     ("closed", "expected"),
@@ -22,12 +18,21 @@ data = {
     ],
 )
 def test_is_between(constructor: Constructor, closed: str, expected: list[bool]) -> None:
+    data = {"a": [1, 4, 2, 5]}
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a").is_between(1, 5, closed=closed))
     expected_dict = {"a": expected}
     assert_equal_data(result, expected_dict)
 
 
+def test_is_between_expressified(constructor: Constructor) -> None:
+    data = {"a": [1, 4, 2, 5], "b": [0, 5, 2, 4], "c": [9, 9, 9, 9]}
+    df = nw.from_native(constructor(data))
+    result = df.select(nw.col("a").is_between(nw.col("b") * 0.9, nw.col("c") - 1))
+    expected_dict = {"a": [True, False, True, True]}
+    assert_equal_data(result, expected_dict)
+
+
 @pytest.mark.parametrize(
     ("closed", "expected"),
     [
@@ -40,7 +45,16 @@ def test_is_between(constructor: Constructor, closed: str, expected: list[bool])
 def test_is_between_series(
     constructor_eager: ConstructorEager, closed: str, expected: list[bool]
 ) -> None:
+    data = {"a": [1, 4, 2, 5]}
     df = nw.from_native(constructor_eager(data), eager_only=True)
     result = df.with_columns(a=df["a"].is_between(1, 5, closed=closed))
     expected_dict = {"a": expected}
     assert_equal_data(result, expected_dict)
+
+
+def test_is_between_expressified_series(constructor_eager: ConstructorEager) -> None:
+    data = {"a": [1, 4, 2, 5], "b": [0, 5, 2, 4], "c": [9, 9, 9, 9]}
+    df = nw.from_native(constructor_eager(data), eager_only=True)
+    result = df["a"].is_between(df["b"], df["c"]).to_frame()
+    expected_dict = {"a": [True, False, True, True]}
+    assert_equal_data(result, expected_dict)