Repository: narwhals-dev/narwhals
Pull request #2163 (2025-03-07 12:45:15)

docs: even more concise docstrings (#2068)
  * series_dt
  * series_cat
  * series_list
  * selectors
  * series_str.py fix
  * group_by


<!--
# 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
- [x] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Related issue #\<issue number\>
- Closes #\<issue number\>

## Checklist

- [x] Code follows style guide (ruff)
- [x] Documented the changes

## If you have comments or can explain your changes, please do so below



Patch:
diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml
index 6749db24..ae69bfe3 100644
--- a/.github/workflows/extremes.yml
+++ b/.github/workflows/extremes.yml
@@ -25,7 +25,7 @@ jobs:
           cache-suffix: ${{ matrix.python-version }}
           cache-dependency-glob: "pyproject.toml"
       - name: install-minimum-versions
-        run: uv pip install pipdeptree tox virtualenv setuptools pandas==0.25.3 polars==0.20.3 numpy==1.17.5 pyarrow==11.0.0 "pyarrow-stubs<17" scipy==1.5.0 scikit-learn==1.1.0 duckdb==1.0 tzdata --system
+        run: uv pip install pipdeptree tox virtualenv setuptools pandas==0.25.3 polars==0.20.3 numpy==1.17.5 pyarrow==11.0.0 "pyarrow-stubs<17" scipy==1.5.0 scikit-learn==1.1.0 duckdb==1.0 tzdata backports.zoneinfo --system
       - name: install-reqs
         run: |
           uv pip install -e ".[tests]" --system
@@ -62,7 +62,7 @@ jobs:
           cache-suffix: ${{ matrix.python-version }}
           cache-dependency-glob: "pyproject.toml"
       - name: install-pretty-old-versions
-        run: uv pip install pipdeptree tox virtualenv setuptools pandas==1.1.5 polars==0.20.3 numpy==1.17.5 pyarrow==11.0.0 "pyarrow-stubs<17" pyspark==3.5.0 scipy==1.5.0 scikit-learn==1.1.0 duckdb==1.0 tzdata --system
+        run: uv pip install pipdeptree tox virtualenv setuptools pandas==1.1.5 polars==0.20.3 numpy==1.17.5 pyarrow==11.0.0 "pyarrow-stubs<17" pyspark==3.5.0 scipy==1.5.0 scikit-learn==1.1.0 duckdb==1.0 tzdata backports.zoneinfo --system
       - name: install-reqs
         run: uv pip install -e ".[tests]" --system
       - name: show-deps
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index e0709a84..6d7cd7a8 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -406,8 +406,8 @@ class ArrowDataFrame(CompliantDataFrame["ArrowSeries"], CompliantLazyFrame):
         other: Self,
         *,
         how: Literal["left", "inner", "cross", "anti", "semi"],
-        left_on: list[str] | None,
-        right_on: list[str] | None,
+        left_on: Sequence[str] | None,
+        right_on: Sequence[str] | None,
         suffix: str,
     ) -> Self:
         how_to_join_map: dict[str, JoinType] = {
@@ -442,8 +442,8 @@ class ArrowDataFrame(CompliantDataFrame["ArrowSeries"], CompliantLazyFrame):
         return self._from_native_frame(
             self._native_frame.join(
                 other._native_frame,
-                keys=left_on or [],
-                right_keys=right_on,
+                keys=left_on or [],  # type: ignore[arg-type]
+                right_keys=right_on,  # type: ignore[arg-type]
                 join_type=how_to_join_map[how],
                 right_suffix=suffix,
             ),
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index f36a43e9..5b808617 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -7,6 +7,8 @@ from typing import Literal
 from typing import Mapping
 from typing import Sequence
 
+import pyarrow.compute as pc
+
 from narwhals._arrow.expr_cat import ArrowExprCatNamespace
 from narwhals._arrow.expr_dt import ArrowExprDateTimeNamespace
 from narwhals._arrow.expr_list import ArrowExprListNamespace
@@ -22,6 +24,7 @@ from narwhals.dependencies import is_numpy_array
 from narwhals.exceptions import ColumnNotFoundError
 from narwhals.typing import CompliantExpr
 from narwhals.utils import Implementation
+from narwhals.utils import generate_temporary_column_name
 from narwhals.utils import not_implemented
 
 if TYPE_CHECKING:
@@ -236,8 +239,8 @@ class ArrowExpr(CompliantExpr["ArrowDataFrame", ArrowSeries]):
 
     def filter(self: Self, *predicates: ArrowExpr) -> Self:
         plx = self.__narwhals_namespace__()
-        other = plx.all_horizontal(*predicates)
-        return reuse_series_implementation(self, "filter", other=other)
+        predicate = plx.all_horizontal(*predicates)
+        return reuse_series_implementation(self, "filter", predicate=predicate)
 
     def mean(self: Self) -> Self:
         return reuse_series_implementation(self, "mean", returns_scalar=True)
@@ -423,28 +426,57 @@ class ArrowExpr(CompliantExpr["ArrowDataFrame", ArrowSeries]):
             self, "clip", lower_bound=lower_bound, upper_bound=upper_bound
         )
 
-    def over(self: Self, keys: Sequence[str], kind: ExprKind) -> Self:
-        if not is_scalar_like(kind):
-            msg = "Only aggregation or literal operations are supported in `over` context for PyArrow."
+    def over(
+        self: Self,
+        partition_by: Sequence[str],
+        kind: ExprKind,
+        order_by: Sequence[str] | None,
+    ) -> Self:
+        if partition_by and not is_scalar_like(kind):
+            msg = "Only aggregation or literal operations are supported in grouped `over` context 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):
-                # E.g. `df.select(nw.all().sum().over('a'))`. This is well-defined,
-                # we just don't support it yet.
-                msg = (
-                    f"Column names {overlap} appear in both expression output names and in `over` keys.\n"
-                    "This is not yet supported."
-                )
-                raise NotImplementedError(msg)
+        if not partition_by:
+            # e.g. `nw.col('a').cum_sum().order_by(key)`
+            # which we can always easily support, as it doesn't require grouping.
+            assert order_by is not None  # help type checkers  # noqa: S101
 
-            tmp = df.group_by(*keys, drop_null_keys=False).agg(self)
-            on = list(keys)
-            tmp = df.simple_select(*keys).join(
-                tmp, how="left", left_on=on, right_on=on, suffix="_right"
-            )
-            return [tmp[alias] for alias in aliases]
+            def func(df: ArrowDataFrame) -> Sequence[ArrowSeries]:
+                token = generate_temporary_column_name(8, df.columns)
+                df = df.with_row_index(token).sort(
+                    *order_by, descending=False, nulls_last=False
+                )
+                result = self(df)
+                # TODO(marco): is there a way to do this efficiently without
+                # doing 2 sorts? Here we're sorting the dataframe and then
+                # again calling `sort_indices`. `ArrowSeries.scatter` would also sort.
+                sorting_indices = pc.sort_indices(df[token]._native_series)  # type: ignore[call-overload]
+                return [
+                    ser._from_native_series(pc.take(ser._native_series, sorting_indices))  # type: ignore[call-overload]
+                    for ser in result
+                ]
+        else:
+
+            def func(df: ArrowDataFrame) -> Sequence[ArrowSeries]:
+                output_names, aliases = evaluate_output_names_and_aliases(self, df, [])
+                if overlap := set(output_names).intersection(partition_by):
+                    # E.g. `df.select(nw.all().sum().over('a'))`. This is well-defined,
+                    # we just don't support it yet.
+                    msg = (
+                        f"Column names {overlap} appear in both expression output names and in `over` keys.\n"
+                        "This is not yet supported."
+                    )
+                    raise NotImplementedError(msg)
+
+                tmp = df.group_by(*partition_by, drop_null_keys=False).agg(self)
+                tmp = df.simple_select(*partition_by).join(
+                    tmp,
+                    how="left",
+                    left_on=partition_by,
+                    right_on=partition_by,
+                    suffix="_right",
+                )
+                return [tmp[alias] for alias in aliases]
 
         return self.__class__(
             func,
@@ -513,11 +545,7 @@ class ArrowExpr(CompliantExpr["ArrowDataFrame", ArrowSeries]):
         return reuse_series_implementation(self, "cum_prod", reverse=reverse)
 
     def rolling_sum(
-        self: Self,
-        window_size: int,
-        *,
-        min_samples: int | None,
-        center: bool,
+        self: Self, window_size: int, *, min_samples: int, center: bool
     ) -> Self:
         return reuse_series_implementation(
             self,
@@ -531,7 +559,7 @@ class ArrowExpr(CompliantExpr["ArrowDataFrame", ArrowSeries]):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         return reuse_series_implementation(
@@ -546,7 +574,7 @@ class ArrowExpr(CompliantExpr["ArrowDataFrame", ArrowSeries]):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -563,7 +591,7 @@ class ArrowExpr(CompliantExpr["ArrowDataFrame", ArrowSeries]):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
diff --git a/narwhals/_arrow/expr_cat.py b/narwhals/_arrow/expr_cat.py
index 6a26ee97..dbea11c5 100644
--- a/narwhals/_arrow/expr_cat.py
+++ b/narwhals/_arrow/expr_cat.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 
+from narwhals._arrow.utils import ArrowExprNamespace
 from narwhals._expression_parsing import reuse_series_namespace_implementation
 
 if TYPE_CHECKING:
@@ -10,11 +11,8 @@ if TYPE_CHECKING:
     from narwhals._arrow.expr import ArrowExpr
 
 
-class ArrowExprCatNamespace:
-    def __init__(self: Self, expr: ArrowExpr) -> None:
-        self._compliant_expr = expr
-
+class ArrowExprCatNamespace(ArrowExprNamespace):
     def get_categories(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "cat", "get_categories"
+            self.compliant, "cat", "get_categories"
         )
diff --git a/narwhals/_arrow/expr_dt.py b/narwhals/_arrow/expr_dt.py
index 30d2e22c..a090d749 100644
--- a/narwhals/_arrow/expr_dt.py
+++ b/narwhals/_arrow/expr_dt.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 
+from narwhals._arrow.utils import ArrowExprNamespace
 from narwhals._expression_parsing import reuse_series_namespace_implementation
 
 if TYPE_CHECKING:
@@ -11,97 +12,84 @@ if TYPE_CHECKING:
     from narwhals.typing import TimeUnit
 
 
-class ArrowExprDateTimeNamespace:
-    def __init__(self: Self, expr: ArrowExpr) -> None:
-        self._compliant_expr = expr
-
+class ArrowExprDateTimeNamespace(ArrowExprNamespace):
     def to_string(self: Self, format: str) -> ArrowExpr:  # noqa: A002
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "to_string", format=format
+            self.compliant, "dt", "to_string", format=format
         )
 
     def replace_time_zone(self: Self, time_zone: str | None) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "replace_time_zone", time_zone=time_zone
+            self.compliant, "dt", "replace_time_zone", time_zone=time_zone
         )
 
     def convert_time_zone(self: Self, time_zone: str) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "convert_time_zone", time_zone=time_zone
+            self.compliant, "dt", "convert_time_zone", time_zone=time_zone
         )
 
     def timestamp(self: Self, time_unit: TimeUnit) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "timestamp", time_unit=time_unit
+            self.compliant, "dt", "timestamp", time_unit=time_unit
         )
 
     def date(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(self._compliant_expr, "dt", "date")
+        return reuse_series_namespace_implementation(self.compliant, "dt", "date")
 
     def year(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(self._compliant_expr, "dt", "year")
+        return reuse_series_namespace_implementation(self.compliant, "dt", "year")
 
     def month(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(self._compliant_expr, "dt", "month")
+        return reuse_series_namespace_implementation(self.compliant, "dt", "month")
 
     def day(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(self._compliant_expr, "dt", "day")
+        return reuse_series_namespace_implementation(self.compliant, "dt", "day")
 
     def hour(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(self._compliant_expr, "dt", "hour")
+        return reuse_series_namespace_implementation(self.compliant, "dt", "hour")
 
     def minute(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(self._compliant_expr, "dt", "minute")
+        return reuse_series_namespace_implementation(self.compliant, "dt", "minute")
 
     def second(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(self._compliant_expr, "dt", "second")
+        return reuse_series_namespace_implementation(self.compliant, "dt", "second")
 
     def millisecond(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "millisecond"
-        )
+        return reuse_series_namespace_implementation(self.compliant, "dt", "millisecond")
 
     def microsecond(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "microsecond"
-        )
+        return reuse_series_namespace_implementation(self.compliant, "dt", "microsecond")
 
     def nanosecond(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "nanosecond"
-        )
+        return reuse_series_namespace_implementation(self.compliant, "dt", "nanosecond")
 
     def ordinal_day(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "ordinal_day"
-        )
+        return reuse_series_namespace_implementation(self.compliant, "dt", "ordinal_day")
 
     def weekday(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "weekday"
-        )
+        return reuse_series_namespace_implementation(self.compliant, "dt", "weekday")
 
     def total_minutes(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "total_minutes"
+            self.compliant, "dt", "total_minutes"
         )
 
     def total_seconds(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "total_seconds"
+            self.compliant, "dt", "total_seconds"
         )
 
     def total_milliseconds(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "total_milliseconds"
+            self.compliant, "dt", "total_milliseconds"
         )
 
     def total_microseconds(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "total_microseconds"
+            self.compliant, "dt", "total_microseconds"
         )
 
     def total_nanoseconds(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "dt", "total_nanoseconds"
+            self.compliant, "dt", "total_nanoseconds"
         )
diff --git a/narwhals/_arrow/expr_list.py b/narwhals/_arrow/expr_list.py
index 8e8e4c1f..14c81f59 100644
--- a/narwhals/_arrow/expr_list.py
+++ b/narwhals/_arrow/expr_list.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 
+from narwhals._arrow.utils import ArrowExprNamespace
 from narwhals._expression_parsing import reuse_series_namespace_implementation
 
 if TYPE_CHECKING:
@@ -10,9 +11,6 @@ if TYPE_CHECKING:
     from narwhals._arrow.expr import ArrowExpr
 
 
-class ArrowExprListNamespace:
-    def __init__(self: Self, expr: ArrowExpr) -> None:
-        self._expr = expr
-
+class ArrowExprListNamespace(ArrowExprNamespace):
     def len(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(self._expr, "list", "len")
+        return reuse_series_namespace_implementation(self.compliant, "list", "len")
diff --git a/narwhals/_arrow/expr_name.py b/narwhals/_arrow/expr_name.py
index 9a7b5491..2f8b3684 100644
--- a/narwhals/_arrow/expr_name.py
+++ b/narwhals/_arrow/expr_name.py
@@ -4,16 +4,15 @@ from typing import TYPE_CHECKING
 from typing import Callable
 from typing import Sequence
 
+from narwhals._arrow.utils import ArrowExprNamespace
+
 if TYPE_CHECKING:
     from typing_extensions import Self
 
     from narwhals._arrow.expr import ArrowExpr
 
 
-class ArrowExprNameNamespace:
-    def __init__(self: Self, expr: ArrowExpr) -> None:
-        self._compliant_expr = expr
-
+class ArrowExprNameNamespace(ArrowExprNamespace):
     def keep(self: Self) -> ArrowExpr:
         return self._from_colname_func_and_alias_output_names(
             name_mapping_func=lambda name: name,
@@ -65,19 +64,18 @@ class ArrowExprNameNamespace:
         name_mapping_func: Callable[[str], str],
         alias_output_names: Callable[[Sequence[str]], Sequence[str]] | None,
     ) -> ArrowExpr:
-        return self._compliant_expr.__class__(
+        return self.compliant.__class__(
             call=lambda df: [
                 series.alias(name_mapping_func(name))
                 for series, name in zip(
-                    self._compliant_expr._call(df),
-                    self._compliant_expr._evaluate_output_names(df),
+                    self.compliant._call(df), self.compliant._evaluate_output_names(df)
                 )
             ],
-            depth=self._compliant_expr._depth,
-            function_name=self._compliant_expr._function_name,
-            evaluate_output_names=self._compliant_expr._evaluate_output_names,
+            depth=self.compliant._depth,
+            function_name=self.compliant._function_name,
+            evaluate_output_names=self.compliant._evaluate_output_names,
             alias_output_names=alias_output_names,
-            backend_version=self._compliant_expr._backend_version,
-            version=self._compliant_expr._version,
-            call_kwargs=self._compliant_expr._call_kwargs,
+            backend_version=self.compliant._backend_version,
+            version=self.compliant._version,
+            call_kwargs=self.compliant._call_kwargs,
         )
diff --git a/narwhals/_arrow/expr_str.py b/narwhals/_arrow/expr_str.py
index 11ba7591..fbabfb55 100644
--- a/narwhals/_arrow/expr_str.py
+++ b/narwhals/_arrow/expr_str.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 
+from narwhals._arrow.utils import ArrowExprNamespace
 from narwhals._expression_parsing import reuse_series_namespace_implementation
 
 if TYPE_CHECKING:
@@ -10,20 +11,15 @@ if TYPE_CHECKING:
     from narwhals._arrow.expr import ArrowExpr
 
 
-class ArrowExprStringNamespace:
-    def __init__(self: Self, expr: ArrowExpr) -> None:
-        self._compliant_expr = expr
-
+class ArrowExprStringNamespace(ArrowExprNamespace):
     def len_chars(self: Self) -> ArrowExpr:
-        return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "len_chars"
-        )
+        return reuse_series_namespace_implementation(self.compliant, "str", "len_chars")
 
     def replace(
         self: Self, pattern: str, value: str, *, literal: bool, n: int
     ) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr,
+            self.compliant,
             "str",
             "replace",
             pattern=pattern,
@@ -34,7 +30,7 @@ class ArrowExprStringNamespace:
 
     def replace_all(self: Self, pattern: str, value: str, *, literal: bool) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr,
+            self.compliant,
             "str",
             "replace_all",
             pattern=pattern,
@@ -44,27 +40,27 @@ class ArrowExprStringNamespace:
 
     def strip_chars(self: Self, characters: str | None) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "strip_chars", characters=characters
+            self.compliant, "str", "strip_chars", characters=characters
         )
 
     def starts_with(self: Self, prefix: str) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "starts_with", prefix=prefix
+            self.compliant, "str", "starts_with", prefix=prefix
         )
 
     def ends_with(self: Self, suffix: str) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "ends_with", suffix=suffix
+            self.compliant, "str", "ends_with", suffix=suffix
         )
 
     def contains(self: Self, pattern: str, *, literal: bool) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "contains", pattern=pattern, literal=literal
+            self.compliant, "str", "contains", pattern=pattern, literal=literal
         )
 
     def slice(self: Self, offset: int, length: int | None) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "slice", offset=offset, length=length
+            self.compliant, "str", "slice", offset=offset, length=length
         )
 
     def split(self: Self, by: str) -> ArrowExpr:
@@ -74,15 +70,15 @@ class ArrowExprStringNamespace:
 
     def to_datetime(self: Self, format: str | None) -> ArrowExpr:  # noqa: A002
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "to_datetime", format=format
+            self.compliant, "str", "to_datetime", format=format
         )
 
     def to_uppercase(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "to_uppercase"
+            self.compliant, "str", "to_uppercase"
         )
 
     def to_lowercase(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
-            self._compliant_expr, "str", "to_lowercase"
+            self.compliant, "str", "to_lowercase"
         )
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 141c98fb..de0d3533 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -28,6 +28,7 @@ from narwhals._arrow.utils import pad_series
 from narwhals.exceptions import InvalidOperationError
 from narwhals.typing import CompliantSeries
 from narwhals.utils import Implementation
+from narwhals.utils import _StoresNative
 from narwhals.utils import generate_temporary_column_name
 from narwhals.utils import import_dtypes_module
 from narwhals.utils import validate_backend_version
@@ -94,7 +95,7 @@ def maybe_extract_py_scalar(value: Any, return_py_scalar: bool) -> Any:  # noqa:
     return value
 
 
-class ArrowSeries(CompliantSeries):
+class ArrowSeries(CompliantSeries, _StoresNative["ArrowChunkedArray"]):
     def __init__(
         self: Self,
         native_series: ArrowChunkedArray,
@@ -113,7 +114,7 @@ class ArrowSeries(CompliantSeries):
 
     def _change_version(self: Self, version: Version) -> Self:
         return self.__class__(
-            self._native_series,
+            self.native,
             name=self._name,
             backend_version=self._backend_version,
             version=version,
@@ -154,7 +155,7 @@ class ArrowSeries(CompliantSeries):
         )
 
     def __len__(self: Self) -> int:
-        return len(self._native_series)
+        return len(self.native)
 
     def __eq__(self: Self, other: object) -> Self:  # type: ignore[override]
         ser, other = extract_native(self, other)
@@ -260,29 +261,33 @@ class ArrowSeries(CompliantSeries):
         return self._from_native_series(res)
 
     def __invert__(self: Self) -> Self:
-        return self._from_native_series(
-            pc.invert(self._native_series)  # type: ignore[call-overload]
-        )
+        return self._from_native_series(pc.invert(self.native))  # type: ignore[call-overload]
 
     @property
     def _type(self: Self) -> pa.DataType:
-        return self._native_series.type
+        return self.native.type
+
+    @property
+    def native(self) -> ArrowChunkedArray:
+        return self._native_series
 
     def len(self: Self, *, _return_py_scalar: bool = True) -> int:
-        return maybe_extract_py_scalar(len(self._native_series), _return_py_scalar)
+        return maybe_extract_py_scalar(len(self.native), _return_py_scalar)
 
-    def filter(self: Self, other: ArrowSeries | list[bool | None]) -> Self:
-        if not (isinstance(other, list) and all(isinstance(x, bool) for x in other)):
-            _, other_native = extract_native(self, other)
+    def filter(self: Self, predicate: ArrowSeries | list[bool | None]) -> Self:
+        if not (
+            isinstance(predicate, list) and all(isinstance(x, bool) for x in predicate)
+        ):
+            _, other_native = extract_native(self, predicate)
         else:
-            other_native = other
-        return self._from_native_series(self._native_series.filter(other_native))  # pyright: ignore[reportArgumentType]
+            other_native = predicate
+        return self._from_native_series(self.native.filter(other_native))  # pyright: ignore[reportArgumentType]
 
     def mean(self: Self, *, _return_py_scalar: bool = True) -> float:
         # NOTE: stub overly strict https://github.com/zen-xu/pyarrow-stubs/blob/d97063876720e6a5edda7eb15f4efe07c31b8296/pyarrow-stubs/compute.pyi#L274-L307
         # docs say numeric https://arrow.apache.org/docs/python/generated/pyarrow.compute.mean.html
         mean: Incomplete = pc.mean
-        return maybe_extract_py_scalar(mean(self._native_series), _return_py_scalar)
+        return maybe_extract_py_scalar(mean(self.native), _return_py_scalar)
 
     def median(self: Self, *, _return_py_scalar: bool = True) -> float:
         from narwhals.exceptions import InvalidOperationError
@@ -292,55 +297,53 @@ class ArrowSeries(CompliantSeries):
             raise InvalidOperationError(msg)
 
         return maybe_extract_py_scalar(
-            pc.approximate_median(self._native_series), _return_py_scalar
+            pc.approximate_median(self.native), _return_py_scalar
         )
 
     def min(self: Self, *, _return_py_scalar: bool = True) -> Any:
-        return maybe_extract_py_scalar(pc.min(self._native_series), _return_py_scalar)
+        return maybe_extract_py_scalar(pc.min(self.native), _return_py_scalar)
 
     def max(self: Self, *, _return_py_scalar: bool = True) -> Any:
-        return maybe_extract_py_scalar(pc.max(self._native_series), _return_py_scalar)
+        return maybe_extract_py_scalar(pc.max(self.native), _return_py_scalar)
 
     def arg_min(self: Self, *, _return_py_scalar: bool = True) -> int:
-        index_min = pc.index(self._native_series, pc.min(self._native_series))
+        index_min = pc.index(self.native, pc.min(self.native))
         return maybe_extract_py_scalar(index_min, _return_py_scalar)
 
     def arg_max(self: Self, *, _return_py_scalar: bool = True) -> int:
-        index_max = pc.index(self._native_series, pc.max(self._native_series))
+        index_max = pc.index(self.native, pc.max(self.native))
         return maybe_extract_py_scalar(index_max, _return_py_scalar)
 
     def sum(self: Self, *, _return_py_scalar: bool = True) -> float:
         return maybe_extract_py_scalar(
-            pc.sum(self._native_series, min_count=0), _return_py_scalar
+            pc.sum(self.native, min_count=0), _return_py_scalar
         )
 
     def drop_nulls(self: Self) -> Self:
-        return self._from_native_series(self._native_series.drop_null())
+        return self._from_native_series(self.native.drop_null())
 
     def shift(self: Self, n: int) -> Self:
-        ca = self._native_series
         if n > 0:
-            arrays = [nulls_like(n, self), *ca[:-n].chunks]
+            arrays = [nulls_like(n, self), *self.native[:-n].chunks]
         elif n < 0:
-            arrays = [*ca[-n:].chunks, nulls_like(-n, self)]
+            arrays = [*self.native[-n:].chunks, nulls_like(-n, self)]
         else:
-            return self._from_native_series(ca)
+            return self._from_native_series(self.native)
         return self._from_native_series(pa.concat_arrays(arrays))
 
     def std(self: Self, ddof: int, *, _return_py_scalar: bool = True) -> float:
         return maybe_extract_py_scalar(
-            pc.stddev(self._native_series, ddof=ddof), _return_py_scalar
+            pc.stddev(self.native, ddof=ddof), _return_py_scalar
         )
 
     def var(self: Self, ddof: int, *, _return_py_scalar: bool = True) -> float:
         return maybe_extract_py_scalar(
-            pc.variance(self._native_series, ddof=ddof), _return_py_scalar
+            pc.variance(self.native, ddof=ddof), _return_py_scalar
         )
 
     def skew(self: Self, *, _return_py_scalar: bool = True) -> float | None:
-        ser = self._native_series
         # NOTE: stub issue with `pc.subtract`, `pc.mean` and `pa.ChunkedArray`
-        ser_not_null: Incomplete = ser.drop_null()
+        ser_not_null: Incomplete = self.native.drop_null()
         if len(ser_not_null) == 0:
             return None
         elif len(ser_not_null) == 1:
@@ -357,12 +360,11 @@ class ArrowSeries(CompliantSeries):
             return maybe_extract_py_scalar(biased_population_skewness, _return_py_scalar)
 
     def count(self: Self, *, _return_py_scalar: bool = True) -> int:
-        return maybe_extract_py_scalar(pc.count(self._native_series), _return_py_scalar)
+        return maybe_extract_py_scalar(pc.count(self.native), _return_py_scalar)
 
     def n_unique(self: Self, *, _return_py_scalar: bool = True) -> int:
-        unique_values = self._native_series.unique()
         return maybe_extract_py_scalar(
-            pc.count(unique_values, mode="all"), _return_py_scalar
+            pc.count(self.native.unique(), mode="all"), _return_py_scalar
         )
 
     def __native_namespace__(self: Self) -> ModuleType:
@@ -391,14 +393,10 @@ class ArrowSeries(CompliantSeries):
         self: Self, idx: int | slice | Sequence[int] | ArrowChunkedArray
     ) -> Any | Self:
         if isinstance(idx, int):
-            return maybe_extract_py_scalar(
-                self._native_series[idx], return_py_scalar=True
-            )
+            return maybe_extract_py_scalar(self.native[idx], return_py_scalar=True)
         if isinstance(idx, (Sequence, pa.ChunkedArray)):
-            return self._from_native_series(
-                self._native_series.take(cast("Indices", idx))
-            )
-        return self._from_native_series(self._native_series[idx])
+            return self._from_native_series(self.native.take(cast("Indices", idx)))
+        return self._from_native_series(self.native[idx])
 
     def scatter(self: Self, indices: int | Sequence[int], values: Any) -> Self:
         import numpy as np  # ignore-banned-import
@@ -411,7 +409,7 @@ class ArrowSeries(CompliantSeries):
             # https://github.com/narwhals-dev/narwhals/issues/2155
             indices_native = pa.array(indices)
             if isinstance(values, self.__class__):
-                values_native = values._native_series.combine_chunks()
+                values_native = values.native.combine_chunks()
             else:
                 values_native = pa.array(values)
 
@@ -422,24 +420,24 @@ class ArrowSeries(CompliantSeries):
         mask: _1DArray = np.zeros(self.len(), dtype=bool)
         mask[indices_native] = True
         result = pc.replace_with_mask(
-            self._native_series,
+            self.native,
             cast("list[bool]", mask),
             values_native.take(cast("Indices", indices_native)),
         )
         return self._from_native_series(result)
 
     def to_list(self: Self) -> list[Any]:
-        return self._native_series.to_pylist()
+        return self.native.to_pylist()
 
     def __array__(self: Self, dtype: Any = None, *, copy: bool | None = None) -> _1DArray:
-        return self._native_series.__array__(dtype=dtype, copy=copy)
+        return self.native.__array__(dtype=dtype, copy=copy)
 
     def to_numpy(self: Self) -> _1DArray:
-        return self._native_series.to_numpy()
+        return self.native.to_numpy()
 
     def alias(self: Self, name: str) -> Self:
         return self.__class__(
-            self._native_series,
+            self.native,
             name=name,
             backend_version=self._backend_version,
             version=self._version,
@@ -447,20 +445,19 @@ class ArrowSeries(CompliantSeries):
 
     @property
     def dtype(self: Self) -> DType:
-        return native_to_narwhals_dtype(self._native_series.type, self._version)
+        return native_to_narwhals_dtype(self.native.type, self._version)
 
     def abs(self: Self) -> Self:
-        return self._from_native_series(pc.abs(self._native_series))
+        return self._from_native_series(pc.abs(self.native))
 
     def cum_sum(self: Self, *, reverse: bool) -> Self:
-        native_series = self._native_series
         # NOTE: stub only permits `NumericArray`
         # https://github.com/zen-xu/pyarrow-stubs/blob/d97063876720e6a5edda7eb15f4efe07c31b8296/pyarrow-stubs/compute.pyi#L140
         cum_sum: Incomplete = pc.cumulative_sum
         result = (
-            cum_sum(native_series, skip_nulls=True)
+            cum_sum(self.native, skip_nulls=True)
             if not reverse
-            else cum_sum(native_series[::-1], skip_nulls=True)[::-1]
+            else cum_sum(self.native[::-1], skip_nulls=True)[::-1]
         )
         return self._from_native_series(result)
 
@@ -469,28 +466,28 @@ class ArrowSeries(CompliantSeries):
         # https://github.com/zen-xu/pyarrow-stubs/blob/d97063876720e6a5edda7eb15f4efe07c31b8296/pyarrow-stubs/compute.pyi#L140
         pc_round: Incomplete = pc.round
         return self._from_native_series(
-            pc_round(self._native_series, decimals, round_mode="half_towards_infinity")
+            pc_round(self.native, decimals, round_mode="half_towards_infinity")
         )
 
     def diff(self: Self) -> Self:
         # NOTE: stub only permits `ChunkedArray[TemporalScalar]`
         # (https://github.com/zen-xu/pyarrow-stubs/blob/d97063876720e6a5edda7eb15f4efe07c31b8296/pyarrow-stubs/compute.pyi#L145-L148)
         diff: Incomplete = pc.pairwise_diff
-        return self._from_native_series(diff(self._native_series.combine_chunks()))
+        return self._from_native_series(diff(self.native.combine_chunks()))
 
     def any(self: Self, *, _return_py_scalar: bool = True) -> bool:
         # NOTE: stub restricts to `BooleanArray`, should be based on truthiness
         # Copies `pc.all`
         pc_any: Incomplete = pc.any
         return maybe_extract_py_scalar(
-            pc_any(self._native_series, min_count=0), _return_py_scalar
+            pc_any(self.native, min_count=0), _return_py_scalar
         )
 
     def all(self: Self, *, _return_py_scalar: bool = True) -> bool:
         # NOTE: stub restricts to `BooleanArray`, should be based on truthiness
         pc_all: Incomplete = pc.all
         return maybe_extract_py_scalar(
-            pc_all(self._native_series, min_count=0), _return_py_scalar
+            pc_all(self.native, min_count=0), _return_py_scalar
         )
 
     def is_between(
@@ -499,73 +496,66 @@ class ArrowSeries(CompliantSeries):
         upper_bound: Any,
         closed: Literal["left", "right", "none", "both"],
     ) -> Self:
-        ser = self._native_series
         _, lower_bound = extract_native(self, lower_bound)
         _, upper_bound = extract_native(self, upper_bound)
         if closed == "left":
-            ge = pc.greater_equal(ser, lower_bound)
-            lt = pc.less(ser, upper_bound)
+            ge = pc.greater_equal(self.native, lower_bound)
+            lt = pc.less(self.native, upper_bound)
             res = pc.and_kleene(ge, lt)
         elif closed == "right":
-            gt = pc.greater(ser, lower_bound)
-            le = pc.less_equal(ser, upper_bound)
+            gt = pc.greater(self.native, lower_bound)
+            le = pc.less_equal(self.native, upper_bound)
             res = pc.and_kleene(gt, le)
         elif closed == "none":
-            gt = pc.greater(ser, lower_bound)
-            lt = pc.less(ser, upper_bound)
+            gt = pc.greater(self.native, lower_bound)
+            lt = pc.less(self.native, upper_bound)
             res = pc.and_kleene(gt, lt)
         elif closed == "both":
-            ge = pc.greater_equal(ser, lower_bound)
-            le = pc.less_equal(ser, upper_bound)
+            ge = pc.greater_equal(self.native, lower_bound)
+            le = pc.less_equal(self.native, upper_bound)
             res = pc.and_kleene(ge, le)
         else:  # pragma: no cover
             raise AssertionError
         return self._from_native_series(res)
 
     def is_null(self: Self) -> Self:
-        ser = self._native_series
-        return self._from_native_series(ser.is_null())
+        return self._from_native_series(self.native.is_null())
 
     def is_nan(self: Self) -> Self:
-        return self._from_native_series(pc.is_nan(self._native_series))
+        return self._from_native_series(pc.is_nan(self.native))
 
     def cast(self: Self, dtype: DType | type[DType]) -> Self:
-        ser = self._native_series
         data_type = narwhals_to_native_dtype(dtype, self._version)
-        return self._from_native_series(pc.cast(ser, data_type))
+        return self._from_native_series(pc.cast(self.native, data_type))
 
     def null_count(self: Self, *, _return_py_scalar: bool = True) -> int:
-        return maybe_extract_py_scalar(self._native_series.null_count, _return_py_scalar)
+        return maybe_extract_py_scalar(self.native.null_count, _return_py_scalar)
 
     def head(self: Self, n: int) -> Self:
-        ser = self._native_series
         if n >= 0:
-            return self._from_native_series(ser.slice(0, n))
+            return self._from_native_series(self.native.slice(0, n))
         else:
-            num_rows = len(ser)
-            return self._from_native_series(ser.slice(0, max(0, num_rows + n)))
+            num_rows = len(self)
+            return self._from_native_series(self.native.slice(0, max(0, num_rows + n)))
 
     def tail(self: Self, n: int) -> Self:
-        ser = self._native_series
         if n >= 0:
-            num_rows = len(ser)
-            return self._from_native_series(ser.slice(max(0, num_rows - n)))
+            num_rows = len(self)
+            return self._from_native_series(self.native.slice(max(0, num_rows - n)))
         else:
-            return self._from_native_series(ser.slice(abs(n)))
+            return self._from_native_series(self.native.slice(abs(n)))
 
     def is_in(self: Self, other: Any) -> Self:
         if isinstance(other, pa.ChunkedArray):
             value_set: ArrowChunkedArray | ArrowArray = other
         else:
             value_set = pa.array(other)
-        ser = self._native_series
-        return self._from_native_series(pc.is_in(ser, value_set=value_set))
+        return self._from_native_series(pc.is_in(self.native, value_set=value_set))
 
     def arg_true(self: Self) -> Self:
         import numpy as np  # ignore-banned-import
 
-        ser = self._native_series
-        res = np.flatnonzero(ser)
+        res = np.flatnonzero(self.native)
         return self._from_iterable(
             res,
             name=self.name,
@@ -581,8 +571,8 @@ class ArrowSeries(CompliantSeries):
                     f" or an explicit index is provided (Series is of length {len(self)})"
                 )
                 raise ValueError(msg)
-            return maybe_extract_py_scalar(self._native_series[0], return_py_scalar=True)
-        return maybe_extract_py_scalar(self._native_series[index], return_py_scalar=True)
+            return maybe_extract_py_scalar(self.native[0], return_py_scalar=True)
+        return maybe_extract_py_scalar(self.native[index], return_py_scalar=True)
 
     def value_counts(
         self: Self,
@@ -598,7 +588,7 @@ class ArrowSeries(CompliantSeries):
         index_name_ = "index" if self._name is None else self._name
         value_name_ = name or ("proportion" if normalize else "count")
 
-        val_counts = pc.value_counts(self._native_series)
+        val_counts = pc.value_counts(self.native)
         values = val_counts.field("values")
         counts = cast("ArrowChunkedArray", val_counts.field("counts"))
 
@@ -620,10 +610,8 @@ class ArrowSeries(CompliantSeries):
         )
 
     def zip_with(self: Self, mask: Self, other: Self) -> Self:
-        cond = mask._native_series.combine_chunks()
-        return self._from_native_series(
-            pc.if_else(cond, self._native_series, other._native_series)
-        )
+        cond = mask.native.combine_chunks()
+        return self._from_native_series(pc.if_else(cond, self.native, other.native))
 
     def sample(
         self: Self,
@@ -635,16 +623,14 @@ class ArrowSeries(CompliantSeries):
     ) -> Self:
         import numpy as np  # ignore-banned-import
 
-        ser = self._native_series
         num_rows = len(self)
-
         if n is None and fraction is not None:
             n = int(num_rows * fraction)
 
         rng = np.random.default_rng(seed=seed)
         idx = np.arange(0, num_rows)
         mask = rng.choice(idx, size=n, replace=with_replacement)
-        return self._from_native_series(ser.take(mask))  # pyright: ignore[reportArgumentType]
+        return self._from_native_series(self.native.take(mask))  # pyright: ignore[reportArgumentType]
 
     def fill_null(
         self: Self,
@@ -678,25 +664,22 @@ class ArrowSeries(CompliantSeries):
                 arr,
             )
 
-        ser = self._native_series
-
         if value is not None:
             _, value = extract_native(self, value)
-            res_ser = self._from_native_series(pc.fill_null(ser, value))  # type: ignore[attr-defined]
+            series = pc.fill_null(self.native, value)  # type: ignore[attr-defined]
         elif limit is None:
             fill_func = (
                 pc.fill_null_forward if strategy == "forward" else pc.fill_null_backward
             )
-            res_ser = self._from_native_series(fill_func(ser))
+            series = fill_func(self.native)
         else:
-            res_ser = self._from_native_series(fill_aux(ser, limit, strategy))
-
-        return res_ser
+            series = fill_aux(self.native, limit, strategy)
+        return self._from_native_series(series)
 
     def to_frame(self: Self) -> ArrowDataFrame:
         from narwhals._arrow.dataframe import ArrowDataFrame
 
-        df = pa.Table.from_arrays([self._native_series], names=[self.name])
+        df = pa.Table.from_arrays([self.native], names=[self.name])
         return ArrowDataFrame(
             df,
             backend_version=self._backend_version,
@@ -707,12 +690,12 @@ class ArrowSeries(CompliantSeries):
     def to_pandas(self: Self) -> pd.Series[Any]:
         import pandas as pd  # ignore-banned-import()
 
-        return pd.Series(self._native_series, name=self.name)  # pyright: ignore[reportArgumentType, reportCallIssue]
+        return pd.Series(self.native, name=self.name)  # pyright: ignore[reportArgumentType, reportCallIssue]
 
     def to_polars(self: Self) -> pl.Series:
         import polars as pl  # ignore-banned-import
 
-        return pl.from_arrow(self._native_series)  # type: ignore[return-value]
+        return pl.from_arrow(self.native)  # type: ignore[return-value]
 
     def is_unique(self: Self) -> Self:
         return self.to_frame().is_unique().alias(self.name)  # type: ignore[return-value]
@@ -723,7 +706,7 @@ class ArrowSeries(CompliantSeries):
         row_number = pa.array(np.arange(len(self)))
         col_token = generate_temporary_column_name(n_bytes=8, columns=[self.name])
         first_distinct_index = (
-            pa.Table.from_arrays([self._native_series], names=[self.name])
+            pa.Table.from_arrays([self.native], names=[self.name])
             .append_column(col_token, row_number)
             .group_by(self.name)
             .aggregate([(col_token, "min")])
@@ -738,7 +721,7 @@ class ArrowSeries(CompliantSeries):
         row_number = pa.array(np.arange(len(self)))
         col_token = generate_temporary_column_name(n_bytes=8, columns=[self.name])
         last_distinct_index = (
-            pa.Table.from_arrays([self._native_series], names=[self.name])
+            pa.Table.from_arrays([self.native], names=[self.name])
             .append_column(col_token, row_number)
             .group_by(self.name)
             .aggregate([(col_token, "max")])
@@ -751,23 +734,21 @@ class ArrowSeries(CompliantSeries):
         if not isinstance(descending, bool):
             msg = f"argument 'descending' should be boolean, found {type(descending)}"
             raise TypeError(msg)
-
-        ser = self._native_series
         if descending:
-            result = pc.all(pc.greater_equal(ser[:-1], ser[1:]))
+            result = pc.all(pc.greater_equal(self.native[:-1], self.native[1:]))
         else:
-            result = pc.all(pc.less_equal(ser[:-1], ser[1:]))
+            result = pc.all(pc.less_equal(self.native[:-1], self.native[1:]))
         return maybe_extract_py_scalar(result, return_py_scalar=True)
 
     def unique(self: Self, *, maintain_order: bool) -> Self:
         # TODO(marco): `pc.unique` seems to always maintain order, is that guaranteed?
-        return self._from_native_series(self._native_series.unique())
+        return self._from_native_series(self.native.unique())
 
     def replace_strict(
         self: Self, old: Sequence[Any], new: Sequence[Any], *, return_dtype: DType | None
     ) -> Self:
         # https://stackoverflow.com/a/79111029/4451315
-        idxs = pc.index_in(self._native_series, pa.array(old))
+        idxs = pc.index_in(self.native, pa.array(old))
         result_native = pc.take(pa.array(new), idxs)
         if return_dtype is not None:
             result_native.cast(narwhals_to_native_dtype(return_dtype, self._version))
@@ -782,23 +763,21 @@ class ArrowSeries(CompliantSeries):
         return result
 
     def sort(self: Self, *, descending: bool, nulls_last: bool) -> Self:
-        series = self._native_series
         order: Order = "descending" if descending else "ascending"
         null_placement: NullPlacement = "at_end" if nulls_last else "at_start"
         sorted_indices = pc.array_sort_indices(
-            series, order=order, null_placement=null_placement
+            self.native, order=order, null_placement=null_placement
         )
-        return self._from_native_series(series.take(sorted_indices))
+        return self._from_native_series(self.native.take(sorted_indices))
 
     def to_dummies(self: Self, *, separator: str, drop_first: bool) -> ArrowDataFrame:
         import numpy as np  # ignore-banned-import
 
         from narwhals._arrow.dataframe import ArrowDataFrame
 
-        series = self._native_series
         name = self._name
         # NOTE: stub is missing attributes (https://arrow.apache.org/docs/python/generated/pyarrow.DictionaryArray.html)
-        da: Incomplete = series.combine_chunks().dictionary_encode(null_encoding="encode")
+        da: Incomplete = self.native.combine_chunks().dictionary_encode("encode")
 
         columns: _2DArray = np.zeros((len(da.dictionary), len(da)), np.int8)
         columns[da.indices, np.arange(len(da))] = 1
@@ -833,29 +812,28 @@ class ArrowSeries(CompliantSeries):
         _return_py_scalar: bool = True,
     ) -> float:
         return maybe_extract_py_scalar(
-            pc.quantile(self._native_series, q=quantile, interpolation=interpolation)[0],
+            pc.quantile(self.native, q=quantile, interpolation=interpolation)[0],
             _return_py_scalar,
         )
 
     def gather_every(self: Self, n: int, offset: int = 0) -> Self:
-        return self._from_native_series(self._native_series[offset::n])
+        return self._from_native_series(self.native[offset::n])
 
     def clip(
         self: Self, lower_bound: Self | Any | None, upper_bound: Self | Any | None
     ) -> Self:
-        arr = self._native_series
         _, lower_bound = extract_native(self, lower_bound)
         _, upper_bound = extract_native(self, upper_bound)
         # NOTE: stubs are missing `ChunkedArray` support
         # https://github.com/zen-xu/pyarrow-stubs/blob/d97063876720e6a5edda7eb15f4efe07c31b8296/pyarrow-stubs/compute.pyi#L948-L954
         max_element_wise: Incomplete = pc.max_element_wise
-        arr = max_element_wise(arr, lower_bound)
+        arr = max_element_wise(self.native, lower_bound)
         arr = cast("ArrowChunkedArray", pc.min_element_wise(arr, upper_bound))
 
         return self._from_native_series(arr)
 
     def to_arrow(self: Self) -> ArrowArray:
-        return self._native_series.combine_chunks()
+        return self.native.combine_chunks()
 
     def mode(self: Self) -> Self:
         plx = self.__narwhals_namespace__()
@@ -868,7 +846,7 @@ class ArrowSeries(CompliantSeries):
         ).filter(plx.col(col_token) == plx.col(col_token).max())[self.name]
 
     def is_finite(self: Self) -> Self:
-        return self._from_native_series(pc.is_finite(self._native_series))
+        return self._from_native_series(pc.is_finite(self.native))
 
     def cum_count(self: Self, *, reverse: bool) -> Self:
         dtypes = import_dtypes_module(self._version)
@@ -879,7 +857,7 @@ class ArrowSeries(CompliantSeries):
             msg = "cum_min method is not supported for pyarrow < 13.0.0"
             raise NotImplementedError(msg)
 
-        native_series = cast("Any", self._native_series)
+        native_series = cast("Any", self.native)
 
         result = (
             pc.cumulative_min(native_series, skip_nulls=True)
@@ -893,7 +871,7 @@ class ArrowSeries(CompliantSeries):
             msg = "cum_max method is not supported for pyarrow < 13.0.0"
             raise NotImplementedError(msg)
 
-        native_series = cast("Any", self._native_series)
+        native_series = cast("Any", self.native)
 
         result = (
             pc.cumulative_max(native_series, skip_nulls=True)
@@ -907,7 +885,7 @@ class ArrowSeries(CompliantSeries):
             msg = "cum_max method is not supported for pyarrow < 13.0.0"
             raise NotImplementedError(msg)
 
-        native_series = cast("Any", self._native_series)
+        native_series = cast("Any", self.native)
 
         result = (
             pc.cumulative_prod(native_series, skip_nulls=True)
@@ -920,7 +898,7 @@ class ArrowSeries(CompliantSeries):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         min_samples = min_samples if min_samples is not None else window_size
@@ -954,7 +932,7 @@ class ArrowSeries(CompliantSeries):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         min_samples = min_samples if min_samples is not None else window_size
@@ -991,7 +969,7 @@ class ArrowSeries(CompliantSeries):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -1044,7 +1022,7 @@ class ArrowSeries(CompliantSeries):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -1075,9 +1053,9 @@ class ArrowSeries(CompliantSeries):
 
         native_series: ArrowChunkedArray | ArrowArray
         if self._backend_version < (14, 0, 0):  # pragma: no cover
-            native_series = self._native_series.combine_chunks()
+            native_series = self.native.combine_chunks()
         else:
-            native_series = self._native_series
+            native_series = self.native
 
         null_mask = pc.is_null(native_series)
 
@@ -1101,7 +1079,7 @@ class ArrowSeries(CompliantSeries):
         from narwhals._arrow.dataframe import ArrowDataFrame
 
         def _hist_from_bin_count(bin_count: int):  # type: ignore[no-untyped-def] # noqa: ANN202
-            d = pc.min_max(self._native_series)
+            d = pc.min_max(self.native)
             lower, upper = d["min"], d["max"]
             pa_float = pa.type_for_alias("float")
             if lower == upper:
@@ -1115,9 +1093,7 @@ class ArrowSeries(CompliantSeries):
                 width = pc.divide(pc.cast(range_, pa_float), lit(float(bin_count)))
 
             bin_proportions = pc.divide(
-                pc.subtract(
-                    cast("pc.NumericOrTemporalArray", self._native_series), lower
-                ),
+                pc.subtract(cast("pc.NumericOrTemporalArray", self.native), lower),
                 width,
             )
             bin_indices: ArrowChunkedArray = cast(
@@ -1164,7 +1140,7 @@ class ArrowSeries(CompliantSeries):
             return counts.column("counts"), bin_right
 
         def _hist_from_bins(bins: Sequence[int | float]):  # type: ignore[no-untyped-def] # noqa: ANN202
-            bin_indices = np.searchsorted(bins, self._native_series, side="left")
+            bin_indices = np.searchsorted(bins, self.native, side="left")
             obs_cats, obs_counts = np.unique(bin_indices, return_counts=True)
             obj_cats = np.arange(1, len(bins))
             counts = np.zeros_like(obj_cats)
@@ -1203,10 +1179,8 @@ class ArrowSeries(CompliantSeries):
         )
 
     def __iter__(self: Self) -> Iterator[Any]:
-        yield from (
-            maybe_extract_py_scalar(x, return_py_scalar=True)
-            for x in self._native_series.__iter__()
-        )
+        for x in self.native:
+            yield maybe_extract_py_scalar(x, return_py_scalar=True)
 
     def __contains__(self: Self, other: Any) -> bool:
         from pyarrow import ArrowInvalid  # ignore-banned-imports
@@ -1214,12 +1188,9 @@ class ArrowSeries(CompliantSeries):
         from pyarrow import ArrowTypeError  # ignore-banned-imports
 
         try:
-            native_series = self._native_series
-            other_ = (
-                lit(other) if other is not None else lit(None, type=native_series.type)
-            )
+            other_ = lit(other) if other is not None else lit(None, type=self._type)
             return maybe_extract_py_scalar(
-                pc.is_in(other_, native_series), return_py_scalar=True
+                pc.is_in(other_, self.native), return_py_scalar=True
             )
         except (ArrowInvalid, ArrowNotImplementedError, ArrowTypeError) as exc:
             from narwhals.exceptions import InvalidOperationError
diff --git a/narwhals/_arrow/series_cat.py b/narwhals/_arrow/series_cat.py
index 73090342..af963e17 100644
--- a/narwhals/_arrow/series_cat.py
+++ b/narwhals/_arrow/series_cat.py
@@ -4,6 +4,8 @@ from typing import TYPE_CHECKING
 
 import pyarrow as pa
 
+from narwhals._arrow.utils import ArrowSeriesNamespace
+
 if TYPE_CHECKING:
     from typing_extensions import Self
 
@@ -11,13 +13,8 @@ if TYPE_CHECKING:
     from narwhals._arrow.typing import Incomplete
 
 
-class ArrowSeriesCatNamespace:
-    def __init__(self: Self, series: ArrowSeries) -> None:
-        self._compliant_series: ArrowSeries = series
-
+class ArrowSeriesCatNamespace(ArrowSeriesNamespace):
     def get_categories(self: Self) -> ArrowSeries:
         # NOTE: Should be `list[pa.DictionaryArray]`, but `DictionaryArray` has no attributes
-        chunks: Incomplete = self._compliant_series._native_series.chunks
-        return self._compliant_series._from_native_series(
-            pa.concat_arrays(x.dictionary for x in chunks).unique()
-        )
+        chunks: Incomplete = self.native.chunks
+        return self.from_native(pa.concat_arrays(x.dictionary for x in chunks).unique())
diff --git a/narwhals/_arrow/series_dt.py b/narwhals/_arrow/series_dt.py
index 6b45a0fa..23dcab43 100644
--- a/narwhals/_arrow/series_dt.py
+++ b/narwhals/_arrow/series_dt.py
@@ -1,11 +1,13 @@
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
+from typing import Any
 from typing import cast
 
 import pyarrow as pa
 import pyarrow.compute as pc
 
+from narwhals._arrow.utils import ArrowSeriesNamespace
 from narwhals._arrow.utils import floordiv_compat
 from narwhals._arrow.utils import lit
 from narwhals.utils import import_dtypes_module
@@ -15,45 +17,43 @@ if TYPE_CHECKING:
 
     from narwhals._arrow.series import ArrowSeries
     from narwhals._arrow.typing import ArrowChunkedArray
+    from narwhals.dtypes import Datetime
     from narwhals.typing import TimeUnit
 
 
-class ArrowSeriesDateTimeNamespace:
-    def __init__(self: Self, series: ArrowSeries) -> None:
-        self._compliant_series: ArrowSeries = series
+class ArrowSeriesDateTimeNamespace(ArrowSeriesNamespace):
+    @property
+    def unit(self) -> TimeUnit:  # NOTE: Unsafe (native).
+        return cast("pa.TimestampType[TimeUnit, Any]", self.native.type).unit
+
+    @property
+    def time_zone(self) -> str | None:  # NOTE: Unsafe (narwhals).
+        return cast("Datetime", self.compliant.dtype).time_zone
 
     def to_string(self: Self, format: str) -> ArrowSeries:  # noqa: A002
         # PyArrow differs from other libraries in that %S also prints out
         # the fractional part of the second...:'(
         # https://arrow.apache.org/docs/python/generated/pyarrow.compute.strftime.html
         format = format.replace("%S.%f", "%S").replace("%S%.f", "%S")
-        return self._compliant_series._from_native_series(
-            pc.strftime(self._compliant_series._native_series, format)
-        )
+        return self.from_native(pc.strftime(self.native, format))
 
     def replace_time_zone(self: Self, time_zone: str | None) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
         if time_zone is not None:
-            result = pc.assume_timezone(pc.local_timestamp(ser._native_series), time_zone)
+            result = pc.assume_timezone(pc.local_timestamp(self.native), time_zone)
         else:
-            result = pc.local_timestamp(ser._native_series)
-        return self._compliant_series._from_native_series(result)
+            result = pc.local_timestamp(self.native)
+        return self.from_native(result)
 
     def convert_time_zone(self: Self, time_zone: str) -> ArrowSeries:
-        if self._compliant_series.dtype.time_zone is None:  # type: ignore[attr-defined]
-            ser: ArrowSeries = self.replace_time_zone("UTC")
-        else:
-            ser = self._compliant_series
-        native_type = pa.timestamp(ser._type.unit, time_zone)  # type: ignore[attr-defined]
-        result = ser._native_series.cast(native_type)
-        return self._compliant_series._from_native_series(result)
+        ser = self.replace_time_zone("UTC") if self.time_zone is None else self.compliant
+        return self.from_native(ser.native.cast(pa.timestamp(self.unit, time_zone)))
 
     def timestamp(self: Self, time_unit: TimeUnit) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
+        ser: ArrowSeries = self.compliant
         dtypes = import_dtypes_module(ser._version)
         if isinstance(ser.dtype, dtypes.Datetime):
             unit = ser.dtype.time_unit
-            s_cast = ser._native_series.cast(pa.int64())
+            s_cast = self.native.cast(pa.int64())
             if unit == "ns":
                 if time_unit == "ns":
                     result = s_cast
@@ -86,7 +86,7 @@ class ArrowSeriesDateTimeNamespace:
                 msg = f"unexpected time unit {unit}, please report an issue at https://github.com/narwhals-dev/narwhals"
                 raise AssertionError(msg)
         elif isinstance(ser.dtype, dtypes.Date):
-            time_s = pc.multiply(ser._native_series.cast(pa.int32()), 86400)
+            time_s = pc.multiply(self.native.cast(pa.int32()), 86400)
             if time_unit == "ns":
                 result = cast("ArrowChunkedArray", pc.multiply(time_s, 1_000_000_000))
             elif time_unit == "us":
@@ -96,148 +96,99 @@ class ArrowSeriesDateTimeNamespace:
         else:
             msg = "Input should be either of Date or Datetime type"
             raise TypeError(msg)
-        return self._compliant_series._from_native_series(result)
+        return self.from_native(result)
 
     def date(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            self._compliant_series._native_series.cast(pa.date32())
-        )
+        return self.from_native(self.native.cast(pa.date32()))
 
     def year(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.year(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.year(self.native))
 
     def month(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.month(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.month(self.native))
 
     def day(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.day(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.day(self.native))
 
     def hour(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.hour(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.hour(self.native))
 
     def minute(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.minute(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.minute(self.native))
 
     def second(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.second(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.second(self.native))
 
     def millisecond(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.millisecond(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.millisecond(self.native))
 
     def microsecond(self: Self) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
-        arr = ser._native_series
+        arr = self.native
         result = pc.add(pc.multiply(pc.millisecond(arr), lit(1000)), pc.microsecond(arr))
-        return self._compliant_series._from_native_series(result)
+        return self.from_native(result)
 
     def nanosecond(self: Self) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
         result = pc.add(
-            pc.multiply(self.microsecond()._native_series, lit(1000)),
-            pc.nanosecond(ser._native_series),
+            pc.multiply(self.microsecond().native, lit(1000)), pc.nanosecond(self.native)
         )
-        return self._compliant_series._from_native_series(result)
+        return self.from_native(result)
 
     def ordinal_day(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.day_of_year(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.day_of_year(self.native))
 
     def weekday(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.day_of_week(self._compliant_series._native_series, count_from_zero=False)
-        )
+        return self.from_native(pc.day_of_week(self.native, count_from_zero=False))
 
     def total_minutes(self: Self) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
         unit_to_minutes_factor = {
             "s": 60,  # seconds
             "ms": 60 * 1e3,  # milli
             "us": 60 * 1e6,  # micro
             "ns": 60 * 1e9,  # nano
         }
-        unit = ser._type.unit  # type: ignore[attr-defined]
-        factor = lit(unit_to_minutes_factor[unit], type=pa.int64())
-        return self._compliant_series._from_native_series(
-            pc.cast(pc.divide(ser._native_series, factor), pa.int64())
-        )
+        factor = lit(unit_to_minutes_factor[self.unit], type=pa.int64())
+        return self.from_native(pc.divide(self.native, factor).cast(pa.int64()))
 
     def total_seconds(self: Self) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
         unit_to_seconds_factor = {
             "s": 1,  # seconds
             "ms": 1e3,  # milli
             "us": 1e6,  # micro
             "ns": 1e9,  # nano
         }
-        unit = ser._type.unit  # type: ignore[attr-defined]
-        factor = lit(unit_to_seconds_factor[unit], type=pa.int64())
-        return self._compliant_series._from_native_series(
-            pc.cast(pc.divide(ser._native_series, factor), pa.int64())
-        )
+        factor = lit(unit_to_seconds_factor[self.unit], type=pa.int64())
+        return self.from_native(pc.divide(self.native, factor).cast(pa.int64()))
 
     def total_milliseconds(self: Self) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
-        arr = ser._native_series
-        unit = ser._type.unit  # type: ignore[attr-defined]
         unit_to_milli_factor = {
             "s": 1e3,  # seconds
             "ms": 1,  # milli
             "us": 1e3,  # micro
             "ns": 1e6,  # nano
         }
-        factor = lit(unit_to_milli_factor[unit], type=pa.int64())
-        if unit == "s":
-            return self._compliant_series._from_native_series(
-                pc.cast(pc.multiply(arr, factor), pa.int64())
-            )
-        return self._compliant_series._from_native_series(
-            pc.cast(pc.divide(arr, factor), pa.int64())
-        )
+        factor = lit(unit_to_milli_factor[self.unit], type=pa.int64())
+        if self.unit == "s":
+            return self.from_native(pc.multiply(self.native, factor).cast(pa.int64()))
+        return self.from_native(pc.divide(self.native, factor).cast(pa.int64()))
 
     def total_microseconds(self: Self) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
-        arr = ser._native_series
-        unit = ser._type.unit  # type: ignore[attr-defined]
         unit_to_micro_factor = {
             "s": 1e6,  # seconds
             "ms": 1e3,  # milli
             "us": 1,  # micro
             "ns": 1e3,  # nano
         }
-        factor = lit(unit_to_micro_factor[unit], type=pa.int64())
-        if unit in {"s", "ms"}:
-            return self._compliant_series._from_native_series(
-                pc.cast(pc.multiply(arr, factor), pa.int64())
-            )
-        return self._compliant_series._from_native_series(
-            pc.cast(pc.divide(arr, factor), pa.int64())
-        )
+        factor = lit(unit_to_micro_factor[self.unit], type=pa.int64())
+        if self.unit in {"s", "ms"}:
+            return self.from_native(pc.multiply(self.native, factor).cast(pa.int64()))
+        return self.from_native(pc.divide(self.native, factor).cast(pa.int64()))
 
     def total_nanoseconds(self: Self) -> ArrowSeries:
-        ser: ArrowSeries = self._compliant_series
         unit_to_nano_factor = {
             "s": 1e9,  # seconds
             "ms": 1e6,  # milli
             "us": 1e3,  # micro
             "ns": 1,  # nano
         }
-        unit = ser._type.unit  # type: ignore[attr-defined]
-        factor = lit(unit_to_nano_factor[unit], type=pa.int64())
-        return self._compliant_series._from_native_series(
-            pc.cast(pc.multiply(ser._native_series, factor), pa.int64())
-        )
+        factor = lit(unit_to_nano_factor[self.unit], type=pa.int64())
+        return self.from_native(pc.multiply(self.native, factor).cast(pa.int64()))
diff --git a/narwhals/_arrow/series_list.py b/narwhals/_arrow/series_list.py
index 05fa3b3f..4c13bdda 100644
--- a/narwhals/_arrow/series_list.py
+++ b/narwhals/_arrow/series_list.py
@@ -5,17 +5,14 @@ from typing import TYPE_CHECKING
 import pyarrow as pa
 import pyarrow.compute as pc
 
+from narwhals._arrow.utils import ArrowSeriesNamespace
+
 if TYPE_CHECKING:
     from typing_extensions import Self
 
     from narwhals._arrow.series import ArrowSeries
 
 
-class ArrowSeriesListNamespace:
-    def __init__(self: Self, series: ArrowSeries) -> None:
-        self._arrow_series: ArrowSeries = series
-
+class ArrowSeriesListNamespace(ArrowSeriesNamespace):
     def len(self: Self) -> ArrowSeries:
-        return self._arrow_series._from_native_series(
-            pc.cast(pc.list_value_length(self._arrow_series._native_series), pa.uint32())
-        )
+        return self.from_native(pc.list_value_length(self.native).cast(pa.uint32()))
diff --git a/narwhals/_arrow/series_str.py b/narwhals/_arrow/series_str.py
index d44e54b2..0026d89a 100644
--- a/narwhals/_arrow/series_str.py
+++ b/narwhals/_arrow/series_str.py
@@ -6,6 +6,7 @@ from typing import Any
 
 import pyarrow.compute as pc
 
+from narwhals._arrow.utils import ArrowSeriesNamespace
 from narwhals._arrow.utils import lit
 from narwhals._arrow.utils import parse_datetime_format
 
@@ -17,22 +18,16 @@ if TYPE_CHECKING:
     from narwhals._arrow.typing import Incomplete
 
 
-class ArrowSeriesStringNamespace:
-    def __init__(self: Self, series: ArrowSeries) -> None:
-        self._compliant_series: ArrowSeries = series
-
+class ArrowSeriesStringNamespace(ArrowSeriesNamespace):
     def len_chars(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.utf8_length(self._compliant_series._native_series)
-        )
+        return self.from_native(pc.utf8_length(self.native))
 
     def replace(
         self: Self, pattern: str, value: str, *, literal: bool, n: int
     ) -> ArrowSeries:
-        compliant = self._compliant_series
         fn = pc.replace_substring if literal else pc.replace_substring_regex
-        arr = fn(compliant._native_series, pattern, replacement=value, max_replacements=n)
-        return compliant._from_native_series(arr)
+        arr = fn(self.native, pattern, replacement=value, max_replacements=n)
+        return self.from_native(arr)
 
     def replace_all(
         self: Self, pattern: str, value: str, *, literal: bool
@@ -40,57 +35,42 @@ class ArrowSeriesStringNamespace:
         return self.replace(pattern, value, literal=literal, n=-1)
 
     def strip_chars(self: Self, characters: str | None) -> ArrowSeries:
-        whitespace = string.whitespace
-        return self._compliant_series._from_native_series(
-            pc.utf8_trim(
-                self._compliant_series._native_series,
-                characters or whitespace,
-            )
+        return self.from_native(
+            pc.utf8_trim(self.native, characters or string.whitespace)
         )
 
     def starts_with(self: Self, prefix: str) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.equal(self.slice(0, len(prefix))._native_series, lit(prefix))
-        )
+        return self.from_native(pc.equal(self.slice(0, len(prefix)).native, lit(prefix)))
 
     def ends_with(self: Self, suffix: str) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.equal(self.slice(-len(suffix), None)._native_series, lit(suffix))
+        return self.from_native(
+            pc.equal(self.slice(-len(suffix), None).native, lit(suffix))
         )
 
     def contains(self: Self, pattern: str, *, literal: bool) -> ArrowSeries:
         check_func = pc.match_substring if literal else pc.match_substring_regex
-        return self._compliant_series._from_native_series(
-            check_func(self._compliant_series._native_series, pattern)
-        )
+        return self.from_native(check_func(self.native, pattern))
 
     def slice(self: Self, offset: int, length: int | None) -> ArrowSeries:
         stop = offset + length if length is not None else None
-        return self._compliant_series._from_native_series(
-            pc.utf8_slice_codeunits(
-                self._compliant_series._native_series, start=offset, stop=stop
-            )
+        return self.from_native(
+            pc.utf8_slice_codeunits(self.native, start=offset, stop=stop)
         )
 
     def split(self: Self, by: str) -> ArrowSeries:
-        split_series = pc.split_pattern(self._compliant_series._native_series, by)  # type: ignore[call-overload]
-        return self._compliant_series._from_native_series(split_series)
+        split_series = pc.split_pattern(self.native, by)  # type: ignore[call-overload]
+        return self.from_native(split_series)
 
     def to_datetime(self: Self, format: str | None) -> ArrowSeries:  # noqa: A002
-        native = self._compliant_series._native_series
-        format = parse_datetime_format(native) if format is None else format
+        format = parse_datetime_format(self.native) if format is None else format
         strptime: Incomplete = pc.strptime
         timestamp_array: pa.Array[pa.TimestampScalar[Any, Any]] = strptime(
-            native, format=format, unit="us"
+            self.native, format=format, unit="us"
         )
-        return self._compliant_series._from_native_series(timestamp_array)
+        return self.from_native(timestamp_array)
 
     def to_uppercase(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.utf8_upper(self._compliant_series._native_series),
-        )
+        return self.from_native(pc.utf8_upper(self.native))
 
     def to_lowercase(self: Self) -> ArrowSeries:
-        return self._compliant_series._from_native_series(
-            pc.utf8_lower(self._compliant_series._native_series),
-        )
+        return self.from_native(pc.utf8_lower(self.native))
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index 577d9b21..bfc617ce 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -12,15 +12,19 @@ from typing import overload
 import pyarrow as pa
 import pyarrow.compute as pc
 
+from narwhals.utils import _ExprNamespace
+from narwhals.utils import _SeriesNamespace
 from narwhals.utils import import_dtypes_module
 from narwhals.utils import isinstance_or_issubclass
 
 if TYPE_CHECKING:
     from typing import TypeVar
 
+    from typing_extensions import Self
     from typing_extensions import TypeAlias
     from typing_extensions import TypeIs
 
+    from narwhals._arrow.expr import ArrowExpr
     from narwhals._arrow.series import ArrowSeries
     from narwhals._arrow.typing import ArrowArray
     from narwhals._arrow.typing import ArrowChunkedArray
@@ -225,22 +229,22 @@ def extract_native(
     from narwhals._arrow.series import ArrowSeries
 
     if rhs is None:
-        return lhs._native_series, lit(None, type=lhs._native_series.type)
+        return lhs.native, lit(None, type=lhs._type)
 
     if isinstance(rhs, ArrowDataFrame):
         return NotImplemented
 
     if isinstance(rhs, ArrowSeries):
         if lhs._broadcast and not rhs._broadcast:
-            return lhs._native_series[0], rhs._native_series
+            return lhs.native[0], rhs.native
         if rhs._broadcast:
-            return lhs._native_series, rhs._native_series[0]
-        return lhs._native_series, rhs._native_series
+            return lhs.native, rhs.native[0]
+        return lhs.native, rhs.native
 
     if isinstance(rhs, list):
         msg = "Expected Series or scalar, got list."
         raise TypeError(msg)
-    return lhs._native_series, rhs
+    return lhs.native, rhs
 
 
 def align_series_full_broadcast(*series: ArrowSeries) -> Sequence[ArrowSeries]:
@@ -255,13 +259,12 @@ def align_series_full_broadcast(*series: ArrowSeries) -> Sequence[ArrowSeries]:
     is_max_length_gt_1 = max_length > 1
     reshaped = []
     for s, length in zip(series, lengths):
-        s_native = s._native_series
         if is_max_length_gt_1 and length == 1:
-            value = s_native[0]
+            value = s.native[0]
             if s._backend_version < (13,) and hasattr(value, "as_py"):
                 value = value.as_py()
             reshaped.append(
-                s._from_native_series(pa.array([value] * max_length, type=s_native.type))
+                s._from_native_series(pa.array([value] * max_length, type=s._type))
             )
         else:
             reshaped.append(s)
@@ -275,17 +278,15 @@ def extract_dataframe_comparand(
     backend_version: tuple[int, ...],
 ) -> ArrowChunkedArray:
     """Extract native Series, broadcasting to `length` if necessary."""
-    import numpy as np  # ignore-banned-import
-
-    if other._broadcast:
-        import numpy as np  # ignore-banned-import
+    if not other._broadcast:
+        return other.native
 
-        value = other._native_series[0]
-        if backend_version < (13,) and hasattr(value, "as_py"):
-            value = value.as_py()
-        return pa.chunked_array([np.full(shape=length, fill_value=value)])
+    import numpy as np  # ignore-banned-import
 
-    return other._native_series
+    value = other.native[0]
+    if backend_version < (13,) and hasattr(value, "as_py"):
+        value = value.as_py()
+    return pa.chunked_array([np.full(shape=length, fill_value=value)])
 
 
 def horizontal_concat(dfs: list[pa.Table]) -> pa.Table:
@@ -533,22 +534,22 @@ def pad_series(
     Returns:
         A tuple containing the padded ArrowSeries and the offset value.
     """
-    if center:
-        offset_left = window_size // 2
-        offset_right = offset_left - (
-            window_size % 2 == 0
-        )  # subtract one if window_size is even
-
-        native_series = series._native_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.chunks, pad_right])
-        )
-        offset = offset_left + offset_right
-    else:
-        padded_arr = series
-        offset = 0
+    if not center:
+        return series, 0
+    offset_left = window_size // 2
+    # subtract one if window_size is even
+    offset_right = offset_left - (window_size % 2 == 0)
+    pad_left = pa.array([None] * offset_left, type=series._type)
+    pad_right = pa.array([None] * offset_right, type=series._type)
+    concat = pa.concat_arrays([pad_left, *series.native.chunks, pad_right])
+    return series._from_native_series(concat), offset_left + offset_right
+
+
+class ArrowSeriesNamespace(_SeriesNamespace["ArrowSeries", "ArrowChunkedArray"]):
+    def __init__(self: Self, series: ArrowSeries, /) -> None:
+        self._compliant_series = series
+
 
-    return padded_arr, offset
+class ArrowExprNamespace(_ExprNamespace["ArrowExpr"]):
+    def __init__(self: Self, expr: ArrowExpr, /) -> None:
+        self._compliant_expr = expr
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index 5cb47377..1158830f 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -26,6 +26,8 @@ from narwhals.utils import generate_temporary_column_name
 from narwhals.utils import not_implemented
 
 if TYPE_CHECKING:
+    from narwhals._expression_parsing import ExprKind
+
     try:
         import dask.dataframe.dask_expr as dx
     except ModuleNotFoundError:
@@ -343,6 +345,7 @@ class DaskExpr(CompliantExpr["DaskLazyFrame", "dx.Series"]):  # pyright: ignore[
 
     def cum_sum(self: Self, *, reverse: bool) -> Self:
         if reverse:  # pragma: no cover
+            # https://github.com/dask/dask/issues/11802
             msg = "`cum_sum(reverse=True)` is not supported with Dask backend"
             raise NotImplementedError(msg)
 
@@ -378,6 +381,16 @@ class DaskExpr(CompliantExpr["DaskLazyFrame", "dx.Series"]):  # pyright: ignore[
 
         return self._from_call(lambda _input: _input.cumprod(), "cum_prod")
 
+    def rolling_sum(
+        self: Self, window_size: int, *, min_samples: int, center: bool
+    ) -> Self:
+        return self._from_call(
+            lambda _input: _input.rolling(
+                window=window_size, min_periods=min_samples, center=center
+            ).sum(),
+            "rolling_sum",
+        )
+
     def sum(self: Self) -> Self:
         return self._from_call(lambda _input: _input.sum().to_series(), "sum")
 
@@ -533,41 +546,58 @@ class DaskExpr(CompliantExpr["DaskLazyFrame", "dx.Series"]):  # pyright: ignore[
             lambda _input: _input.isna().sum().to_series(), "null_count"
         )
 
-    def over(self: Self, keys: Sequence[str], kind: ExprKind) -> Self:
+    def over(
+        self: Self,
+        partition_by: Sequence[str],
+        kind: ExprKind,
+        order_by: Sequence[str] | None,
+    ) -> Self:
         # pandas is a required dependency of dask so it's safe to import this
         from narwhals._pandas_like.group_by import AGGREGATIONS_TO_PANDAS_EQUIVALENT
 
-        if not is_elementary_expression(self):  # pragma: no cover
+        if not partition_by:
+            assert order_by is not None  # help type checkers  # noqa: S101
+
+            # This is something like `nw.col('a').cum_sum().order_by(key)`
+            # which we can always easily support, as it doesn't require grouping.
+            def func(df: DaskLazyFrame) -> Sequence[dx.Series]:
+                return self(df.sort(*order_by, descending=False, nulls_last=False))
+        elif not is_elementary_expression(self):  # pragma: no cover
             msg = (
                 "Only elementary expressions are supported for `.over` in dask.\n\n"
                 "Please see: "
                 "https://narwhals-dev.github.io/narwhals/pandas_like_concepts/improve_group_by_operation/"
             )
             raise NotImplementedError(msg)
-        function_name = re.sub(r"(\w+->)", "", self._function_name)
-        try:
-            dask_function_name = AGGREGATIONS_TO_PANDAS_EQUIVALENT[function_name]
-        except KeyError:
-            msg = (
-                f"Unsupported function: {function_name} in `over` context.\n\n."
-                f"Supported functions are {', '.join(AGGREGATIONS_TO_PANDAS_EQUIVALENT)}\n"
-            )
-            raise NotImplementedError(msg) from None
-
-        def func(df: DaskLazyFrame) -> list[dx.Series]:
-            output_names, aliases = evaluate_output_names_and_aliases(self, df, [])
-
-            with warnings.catch_warnings():
-                warnings.filterwarnings(
-                    "ignore", message=".*`meta` is not specified", category=UserWarning
-                )
-                res_native = df._native_frame.groupby(keys)[list(output_names)].transform(
-                    dask_function_name, **self._call_kwargs
+        else:
+            function_name = re.sub(r"(\w+->)", "", self._function_name)
+            try:
+                dask_function_name = AGGREGATIONS_TO_PANDAS_EQUIVALENT[function_name]
+            except KeyError:
+                # window functions are unsupported: https://github.com/dask/dask/issues/11806
+                msg = (
+                    f"Unsupported function: {function_name} in `over` context.\n\n"
+                    f"Supported functions are {', '.join(AGGREGATIONS_TO_PANDAS_EQUIVALENT)}\n"
                 )
-            result_frame = df._from_native_frame(
-                res_native.rename(columns=dict(zip(output_names, aliases)))
-            )._native_frame
-            return [result_frame[name] for name in aliases]
+                raise NotImplementedError(msg) from None
+
+            def func(df: DaskLazyFrame) -> Sequence[dx.Series]:
+                output_names, aliases = evaluate_output_names_and_aliases(self, df, [])
+
+                with warnings.catch_warnings():
+                    # https://github.com/dask/dask/issues/11804
+                    warnings.filterwarnings(
+                        "ignore",
+                        message=".*`meta` is not specified",
+                        category=UserWarning,
+                    )
+                    res_native = df._native_frame.groupby(partition_by)[
+                        list(output_names)
+                    ].transform(dask_function_name, **self._call_kwargs)
+                result_frame = df._from_native_frame(
+                    res_native.rename(columns=dict(zip(output_names, aliases)))
+                )._native_frame
+                return [result_frame[name] for name in aliases]
 
         return self.__class__(
             func,
@@ -614,7 +644,6 @@ class DaskExpr(CompliantExpr["DaskLazyFrame", "dx.Series"]):  # pyright: ignore[
     sample = not_implemented()
     map_batches = not_implemented()
     ewm_mean = not_implemented()
-    rolling_sum = not_implemented()
     rolling_mean = not_implemented()
     rolling_var = not_implemented()
     rolling_std = not_implemented()
diff --git a/narwhals/_duckdb/expr_str.py b/narwhals/_duckdb/expr_str.py
index b7804662..6f298984 100644
--- a/narwhals/_duckdb/expr_str.py
+++ b/narwhals/_duckdb/expr_str.py
@@ -108,7 +108,7 @@ class DuckDBExprStringNamespace:
 
     def to_datetime(self: Self, format: str | None) -> DuckDBExpr:  # noqa: A002
         if format is None:
-            msg = "Cannot infer format with DuckDB backend"
+            msg = "Cannot infer format with DuckDB backend, please specify `format` explicitly."
             raise NotImplementedError(msg)
 
         return self._compliant_expr._from_call(
diff --git a/narwhals/_duckdb/utils.py b/narwhals/_duckdb/utils.py
index 5fa20527..bffccac4 100644
--- a/narwhals/_duckdb/utils.py
+++ b/narwhals/_duckdb/utils.py
@@ -83,6 +83,10 @@ def native_to_narwhals_dtype(duckdb_dtype: str, version: Version) -> DType:
         return dtypes.Date()
     if duckdb_dtype == "TIMESTAMP":
         return dtypes.Datetime()
+    if duckdb_dtype == "TIMESTAMP WITH TIME ZONE":
+        # TODO(marco): is UTC correct, or should we be getting the connection timezone?
+        # https://github.com/narwhals-dev/narwhals/issues/2165
+        return dtypes.Datetime(time_zone="UTC")
     if duckdb_dtype == "BOOLEAN":
         return dtypes.Boolean()
     if duckdb_dtype == "INTERVAL":
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index 14b4625a..f23b1996 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -19,11 +19,11 @@ from narwhals._pandas_like.expr_name import PandasLikeExprNameNamespace
 from narwhals._pandas_like.expr_str import PandasLikeExprStringNamespace
 from narwhals._pandas_like.group_by import AGGREGATIONS_TO_PANDAS_EQUIVALENT
 from narwhals._pandas_like.series import PandasLikeSeries
-from narwhals._pandas_like.utils import rename
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import is_numpy_array
 from narwhals.exceptions import ColumnNotFoundError
 from narwhals.typing import CompliantExpr
+from narwhals.utils import generate_temporary_column_name
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -43,6 +43,7 @@ WINDOW_FUNCTIONS_TO_PANDAS_EQUIVALENT = {
     # Pandas cumcount counts nulls while Polars does not
     # So, instead of using "cumcount" we use "cumsum" on notna() to get the same result
     "cum_count": "cumsum",
+    "rolling_sum": "sum",
     "shift": "shift",
     "rank": "rank",
     "diff": "diff",
@@ -64,6 +65,12 @@ def window_kwargs_to_pandas_equivalent(
         }
     elif function_name.startswith("cum_"):  # Cumulative operation
         pandas_kwargs = {"skipna": True}
+    elif function_name.startswith("rolling_"):  # Rolling operation
+        pandas_kwargs = {
+            "min_periods": kwargs["min_samples"],
+            "window": kwargs["window_size"],
+            "center": kwargs["center"],
+        }
     else:  # e.g. std, var
         pandas_kwargs = kwargs
     return pandas_kwargs
@@ -380,8 +387,8 @@ class PandasLikeExpr(CompliantExpr["PandasLikeDataFrame", PandasLikeSeries]):
 
     def filter(self: Self, *predicates: PandasLikeExpr) -> Self:
         plx = self.__narwhals_namespace__()
-        other = plx.all_horizontal(*predicates)
-        return reuse_series_implementation(self, "filter", other=other)
+        predicate = plx.all_horizontal(*predicates)
+        return reuse_series_implementation(self, "filter", predicate=predicate)
 
     def drop_nulls(self: Self) -> Self:
         return reuse_series_implementation(self, "drop_nulls")
@@ -457,63 +464,96 @@ class PandasLikeExpr(CompliantExpr["PandasLikeDataFrame", PandasLikeSeries]):
             call_kwargs=self._call_kwargs,
         )
 
-    def over(self: Self, partition_by: Sequence[str], kind: ExprKind) -> Self:
-        if not is_elementary_expression(self):
+    def over(
+        self: Self,
+        partition_by: Sequence[str],
+        kind: ExprKind,
+        order_by: Sequence[str] | None,
+    ) -> Self:
+        if not partition_by:
+            # e.g. `nw.col('a').cum_sum().order_by(key)`
+            # We can always easily support this as it doesn't require grouping.
+            assert order_by is not None  # noqa: S101  # help type-check
+
+            def func(df: PandasLikeDataFrame) -> Sequence[PandasLikeSeries]:
+                token = generate_temporary_column_name(8, df.columns)
+                df = df.with_row_index(token).sort(
+                    *order_by, descending=False, nulls_last=False
+                )
+                results = self(df)
+                sorting_indices = df[token]
+                for s in results:
+                    s._scatter_in_place(sorting_indices, s)
+                return results
+        elif not is_elementary_expression(self):
             msg = (
                 "Only elementary expressions are supported for `.over` in pandas-like backends.\n\n"
                 "Please see: "
                 "https://narwhals-dev.github.io/narwhals/pandas_like_concepts/improve_group_by_operation/"
             )
             raise NotImplementedError(msg)
-        function_name = re.sub(r"(\w+->)", "", self._function_name)
-        try:
-            pandas_function_name = WINDOW_FUNCTIONS_TO_PANDAS_EQUIVALENT[function_name]
-        except KeyError:
-            try:
-                pandas_function_name = AGGREGATIONS_TO_PANDAS_EQUIVALENT[function_name]
-            except KeyError:
+        else:
+            function_name: str = re.sub(r"(\w+->)", "", self._function_name)
+            pandas_function_name = WINDOW_FUNCTIONS_TO_PANDAS_EQUIVALENT.get(
+                function_name,
+                AGGREGATIONS_TO_PANDAS_EQUIVALENT.get(function_name),
+            )
+            if pandas_function_name is None:
                 msg = (
                     f"Unsupported function: {function_name} in `over` context.\n\n"
                     f"Supported functions are {', '.join(WINDOW_FUNCTIONS_TO_PANDAS_EQUIVALENT)}\n"
                     f"and {', '.join(AGGREGATIONS_TO_PANDAS_EQUIVALENT)}."
                 )
-                raise NotImplementedError(msg) from None
-
-        def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
-            output_names, aliases = evaluate_output_names_and_aliases(self, df, [])
+                raise NotImplementedError(msg)
             pandas_kwargs = window_kwargs_to_pandas_equivalent(
                 function_name, self._call_kwargs
             )
 
-            if function_name == "cum_count":
-                plx = self.__narwhals_namespace__()
-                df = df.with_columns(~plx.col(*output_names).is_null())
-            if function_name.startswith("cum_"):
-                reverse = self._call_kwargs["reverse"]
-            else:
-                assert "reverse" not in self._call_kwargs  # debug assertion  # noqa: S101
-                reverse = False
-            if reverse:
-                # Only select the columns we need to avoid reversing columns
-                # unnecessarily
-                columns = list(set(partition_by).union(output_names))
-                native_frame = df[columns]._native_frame[::-1]
-            else:
-                native_frame = df._native_frame
-            res_native = native_frame.groupby(partition_by)[list(output_names)].transform(
-                pandas_function_name, **pandas_kwargs
-            )
-            result_frame = df._from_native_frame(
-                rename(
-                    res_native,
-                    columns=dict(zip(output_names, aliases)),
-                    implementation=self._implementation,
-                    backend_version=self._backend_version,
+            def func(df: PandasLikeDataFrame) -> Sequence[PandasLikeSeries]:
+                output_names, aliases = evaluate_output_names_and_aliases(self, df, [])
+                if function_name == "cum_count":
+                    plx = self.__narwhals_namespace__()
+                    df = df.with_columns(~plx.col(*output_names).is_null())
+
+                if function_name.startswith("cum_"):
+                    reverse = self._call_kwargs["reverse"]
+                else:
+                    assert "reverse" not in self._call_kwargs  # noqa: S101
+                    reverse = False
+
+                if order_by:
+                    columns = list(set(partition_by).union(output_names).union(order_by))
+                    token = generate_temporary_column_name(8, columns)
+                    df = (
+                        df[columns]
+                        .with_row_index(token)
+                        .sort(*order_by, descending=reverse, nulls_last=reverse)
+                    )
+                    sorting_indices = df[token]
+                elif reverse:
+                    columns = list(set(partition_by).union(output_names))
+                    df = df[columns][::-1]
+                if function_name.startswith("rolling"):
+                    rolling = df._native_frame.groupby(partition_by)[
+                        list(output_names)
+                    ].rolling(**pandas_kwargs)
+                    assert pandas_function_name is not None  # help mypy  # noqa: S101
+                    res_native = getattr(rolling, pandas_function_name)()
+                else:
+                    res_native = df._native_frame.groupby(partition_by)[
+                        list(output_names)
+                    ].transform(pandas_function_name, **pandas_kwargs)
+                result_frame = df._from_native_frame(res_native).rename(
+                    dict(zip(output_names, aliases))
                 )
-            )
-            if reverse:
-                return [result_frame[name][::-1] for name in aliases]
-            return [result_frame[name] for name in aliases]
+                results = [result_frame[name] for name in aliases]
+                if order_by:
+                    for s in results:
+                        s._scatter_in_place(sorting_indices, s)
+                    return results
+                if reverse:
+                    return [s[::-1] for s in results]
+                return results
 
         return self.__class__(
             func,
@@ -623,25 +663,23 @@ class PandasLikeExpr(CompliantExpr["PandasLikeDataFrame", PandasLikeSeries]):
         )
 
     def rolling_sum(
-        self: Self,
-        window_size: int,
-        *,
-        min_samples: int | None,
-        center: bool,
+        self: Self, window_size: int, *, min_samples: int, center: bool
     ) -> Self:
         return reuse_series_implementation(
             self,
             "rolling_sum",
-            window_size=window_size,
-            min_samples=min_samples,
-            center=center,
+            call_kwargs={
+                "window_size": window_size,
+                "min_samples": min_samples,
+                "center": center,
+            },
         )
 
     def rolling_mean(
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         return reuse_series_implementation(
@@ -656,7 +694,7 @@ class PandasLikeExpr(CompliantExpr["PandasLikeDataFrame", PandasLikeSeries]):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -673,7 +711,7 @@ class PandasLikeExpr(CompliantExpr["PandasLikeDataFrame", PandasLikeSeries]):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index ea05c77a..7a0a0d3c 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -9,6 +9,8 @@ from typing import Sequence
 from typing import cast
 from typing import overload
 
+import numpy as np
+
 from narwhals._pandas_like.series_cat import PandasLikeSeriesCatNamespace
 from narwhals._pandas_like.series_dt import PandasLikeSeriesDateTimeNamespace
 from narwhals._pandas_like.series_list import PandasLikeSeriesListNamespace
@@ -27,6 +29,7 @@ from narwhals.exceptions import InvalidOperationError
 from narwhals.typing import CompliantSeries
 from narwhals.utils import Implementation
 from narwhals.utils import import_dtypes_module
+from narwhals.utils import parse_version
 from narwhals.utils import validate_backend_version
 
 if TYPE_CHECKING:
@@ -177,7 +180,7 @@ class PandasLikeSeries(CompliantSeries):
         )
 
     def __len__(self: Self) -> int:
-        return len(self._native_series)
+        return len(self.native)
 
     @property
     def name(self: Self) -> str:
@@ -185,15 +188,19 @@ class PandasLikeSeries(CompliantSeries):
 
     @property
     def dtype(self: Self) -> DType:
-        native_dtype = self._native_series.dtype
+        native_dtype = self.native.dtype
         return (
             native_to_narwhals_dtype(native_dtype, self._version, self._implementation)
             if native_dtype != "object"
             else object_native_to_narwhals_dtype(
-                self._native_series, self._version, self._implementation
+                self.native, self._version, self._implementation
             )
         )
 
+    @property
+    def native(self) -> Any:
+        return self._native_series
+
     def ewm_mean(
         self: Self,
         *,
@@ -227,11 +234,8 @@ class PandasLikeSeries(CompliantSeries):
 
     def scatter(self: Self, indices: int | Sequence[int], values: Any) -> Self:
         if isinstance(values, self.__class__):
-            # .copy() is necessary in some pre-2.2 versions of pandas to avoid
-            # `values` also getting modified (!)
-            _, values = align_and_extract_native(self, values)
             values = set_index(
-                values.copy(),
+                values._native_series,
                 self._native_series.index[indices],
                 implementation=self._implementation,
                 backend_version=self._backend_version,
@@ -241,6 +245,25 @@ class PandasLikeSeries(CompliantSeries):
         s.name = self.name
         return self._from_native_series(s)
 
+    def _scatter_in_place(self: Self, indices: Self, values: Self) -> None:
+        # Scatter, modifying original Series. Use with care!
+        values_native = set_index(
+            values._native_series,
+            self._native_series.index[indices._native_series],
+            implementation=self._implementation,
+            backend_version=self._backend_version,
+        )
+        if self._implementation is Implementation.PANDAS and parse_version(np) < (2,):
+            values_native = values_native.copy()  # pragma: no cover
+        min_pd_version = (1, 2)
+        if (
+            self._implementation is Implementation.PANDAS
+            and self._backend_version < min_pd_version
+        ):
+            self._native_series.iloc[indices._native_series.values] = values_native  # noqa: PD011
+        else:
+            self._native_series.iloc[indices._native_series] = values_native
+
     def cast(self: Self, dtype: DType | type[DType]) -> Self:
         ser = self._native_series
         dtype_backend = get_dtype_backend(
@@ -328,11 +351,13 @@ class PandasLikeSeries(CompliantSeries):
 
     # Binary comparisons
 
-    def filter(self: Self, other: Any) -> PandasLikeSeries:
-        if not (isinstance(other, list) and all(isinstance(x, bool) for x in other)):
-            _, other_native = align_and_extract_native(self, other)
+    def filter(self: Self, predicate: Any) -> PandasLikeSeries:
+        if not (
+            isinstance(predicate, list) and all(isinstance(x, bool) for x in predicate)
+        ):
+            _, other_native = align_and_extract_native(self, predicate)
         else:
-            other_native = other
+            other_native = predicate
         return self._from_native_series(self._native_series.loc[other_native]).alias(
             self.name
         )
@@ -897,7 +922,7 @@ class PandasLikeSeries(CompliantSeries):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         result = self._native_series.rolling(
@@ -909,7 +934,7 @@ class PandasLikeSeries(CompliantSeries):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         result = self._native_series.rolling(
@@ -921,7 +946,7 @@ class PandasLikeSeries(CompliantSeries):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -934,7 +959,7 @@ class PandasLikeSeries(CompliantSeries):
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
diff --git a/narwhals/_pandas_like/series_str.py b/narwhals/_pandas_like/series_str.py
index 5fae5867..6e759084 100644
--- a/narwhals/_pandas_like/series_str.py
+++ b/narwhals/_pandas_like/series_str.py
@@ -85,11 +85,23 @@ class PandasLikeSeriesStringNamespace:
         )
 
     def to_datetime(self: Self, format: str | None) -> PandasLikeSeries:  # noqa: A002
-        return self._compliant_series._from_native_series(
-            to_datetime(self._compliant_series._implementation)(
+        if format is not None and any(x in format for x in ("%z", "Z")):
+            # We know that the inputs are timezone-aware, so we can directly pass
+            # `utc=True` for better performance.
+            return self._compliant_series._from_native_series(
+                to_datetime(self._compliant_series._implementation, utc=True)(
+                    self._compliant_series._native_series, format=format
+                )
+            )
+        result = self._compliant_series._from_native_series(
+            to_datetime(self._compliant_series._implementation, utc=False)(
                 self._compliant_series._native_series, format=format
             )
         )
+        result_time_zone = result.dtype.time_zone  # type: ignore[attr-defined]
+        if result_time_zone is not None and result_time_zone != "UTC":
+            result = result.dt.convert_time_zone("UTC")
+        return result
 
     def to_uppercase(self: Self) -> PandasLikeSeries:
         return self._compliant_series._from_native_series(
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 51fa5108..a442c640 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -1,9 +1,9 @@
 from __future__ import annotations
 
+import functools
 import re
 import warnings
 from contextlib import suppress
-from functools import lru_cache
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
@@ -356,7 +356,7 @@ def rename(
     return obj.rename(*args, **kwargs, copy=False)  # type: ignore[attr-defined]
 
 
-@lru_cache(maxsize=16)
+@functools.lru_cache(maxsize=16)
 def non_object_native_to_narwhals_dtype(dtype: str, version: Version) -> DType:
     dtypes = import_dtypes_module(version)
     if dtype in {"int64", "Int64", "Int64[pyarrow]", "int64[pyarrow]"}:
@@ -679,9 +679,11 @@ def align_series_full_broadcast(
     return reindexed
 
 
-def to_datetime(implementation: Implementation) -> Any:
+def to_datetime(implementation: Implementation, *, utc: bool) -> Any:
     if implementation in PANDAS_LIKE_IMPLEMENTATION:
-        return implementation.to_native_namespace().to_datetime
+        return functools.partial(
+            implementation.to_native_namespace().to_datetime, utc=utc
+        )
 
     else:  # pragma: no cover
         msg = f"Expected pandas-like implementation ({PANDAS_LIKE_IMPLEMENTATION}), found {implementation}"
diff --git a/narwhals/_polars/expr.py b/narwhals/_polars/expr.py
index d39d2cdb..fbb96447 100644
--- a/narwhals/_polars/expr.py
+++ b/narwhals/_polars/expr.py
@@ -97,14 +97,28 @@ 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 over(
+        self: Self,
+        partition_by: Sequence[str],
+        kind: ExprKind,
+        order_by: Sequence[str] | None,
+    ) -> Self:
+        if self._backend_version < (1, 9):
+            if order_by:
+                msg = "`order_by` in Polars requires version 1.10 or greater"
+                raise NotImplementedError(msg)
+            return self._from_native_expr(
+                self._native_expr.over(partition_by or pl.lit(1))
+            )
+        return self._from_native_expr(
+            self._native_expr.over(partition_by or pl.lit(1), order_by=order_by)
+        )
 
     def rolling_var(
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -130,7 +144,7 @@ class PolarsExpr:
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -153,11 +167,7 @@ class PolarsExpr:
         )
 
     def rolling_sum(
-        self: Self,
-        window_size: int,
-        *,
-        min_samples: int | None,
-        center: bool,
+        self: Self, window_size: int, *, min_samples: int, center: bool
     ) -> Self:
         extra_kwargs = (
             {"min_periods": min_samples}
@@ -177,7 +187,7 @@ class PolarsExpr:
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         extra_kwargs = (
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index 73133a3b..1db4d8bb 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -115,6 +115,10 @@ class PolarsSeries:
             self._native_series.dtype, self._version, self._backend_version
         )
 
+    @property
+    def native(self) -> pl.Series:
+        return self._native_series
+
     def alias(self, name: str) -> Self:
         return self._from_native_object(self._native_series.alias(name))
 
@@ -130,9 +134,8 @@ class PolarsSeries:
         return self._from_native_object(self._native_series.__getitem__(item))
 
     def cast(self: Self, dtype: DType) -> Self:
-        ser = self._native_series
         dtype_pl = narwhals_to_native_dtype(dtype, self._version, self._backend_version)
-        return self._from_native_series(ser.cast(dtype_pl))
+        return self._from_native_series(self.native.cast(dtype_pl))
 
     def replace_strict(
         self: Self, old: Sequence[Any], new: Sequence[Any], *, return_dtype: DType | None
@@ -306,7 +309,7 @@ class PolarsSeries:
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -333,7 +336,7 @@ class PolarsSeries:
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self:
@@ -360,7 +363,7 @@ class PolarsSeries:
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         extra_kwargs = (
@@ -381,7 +384,7 @@ class PolarsSeries:
         self: Self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self:
         extra_kwargs = (
diff --git a/narwhals/_spark_like/expr.py b/narwhals/_spark_like/expr.py
index 7c487314..32f1923e 100644
--- a/narwhals/_spark_like/expr.py
+++ b/narwhals/_spark_like/expr.py
@@ -24,10 +24,12 @@ from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
     from pyspark.sql import Column
+    from pyspark.sql import Window
     from typing_extensions import Self
 
     from narwhals._spark_like.dataframe import SparkLikeLazyFrame
     from narwhals._spark_like.namespace import SparkLikeNamespace
+    from narwhals._spark_like.typing import WindowFunction
     from narwhals.dtypes import DType
     from narwhals.utils import Version
 
@@ -53,6 +55,7 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
         self._backend_version = backend_version
         self._version = version
         self._implementation = implementation
+        self._window_function: WindowFunction | None = None
 
     def __call__(self: Self, df: SparkLikeLazyFrame) -> Sequence[Column]:
         return self._call(df)
@@ -78,7 +81,11 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
         )
 
     @property
-    def _F(self: Self) -> Any:  # noqa: N802
+    def _F(self: Self):  # type: ignore[no-untyped-def] # noqa: ANN202, N802
+        if TYPE_CHECKING:
+            from pyspark.sql import functions
+
+            return functions
         if self._implementation is Implementation.SQLFRAME:
             from sqlframe.base.session import _BaseSession
 
@@ -91,7 +98,11 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
         return functions
 
     @property
-    def _native_dtypes(self: Self) -> Any:
+    def _native_dtypes(self: Self):  # type: ignore[no-untyped-def] # noqa: ANN202
+        if TYPE_CHECKING:
+            from pyspark.sql import types
+
+            return types
         if self._implementation is Implementation.SQLFRAME:
             from sqlframe.base.session import _BaseSession
 
@@ -104,7 +115,7 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
         return types
 
     @property
-    def _Window(self: Self) -> Any:  # noqa: N802
+    def _Window(self: Self) -> type[Window]:  # noqa: N802
         if self._implementation is Implementation.SQLFRAME:
             from sqlframe.base.session import _BaseSession
 
@@ -202,6 +213,22 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
             implementation=self._implementation,
         )
 
+    def _with_window_function(
+        self: Self,
+        window_function: WindowFunction,
+    ) -> Self:
+        result = self.__class__(
+            self._call,
+            function_name=self._function_name,
+            evaluate_output_names=self._evaluate_output_names,
+            alias_output_names=self._alias_output_names,
+            backend_version=self._backend_version,
+            version=self._version,
+            implementation=self._implementation,
+        )
+        result._window_function = window_function
+        return result
+
     def __eq__(self: Self, other: SparkLikeExpr) -> Self:  # type: ignore[override]
         return self._from_call(
             lambda _input, other: _input.__eq__(other), "__eq__", other=other
@@ -487,9 +514,27 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
 
         return self._from_call(_n_unique, "n_unique")
 
-    def over(self: Self, keys: Sequence[str], kind: ExprKind) -> Self:
-        def func(df: SparkLikeLazyFrame) -> list[Column]:
-            return [expr.over(self._Window.partitionBy(*keys)) for expr in self._call(df)]
+    def over(
+        self: Self,
+        partition_by: Sequence[str],
+        kind: ExprKind,
+        order_by: Sequence[str] | None,
+    ) -> Self:
+        if (window_function := self._window_function) is not None:
+            assert order_by is not None  # noqa: S101
+
+            def func(df: SparkLikeLazyFrame) -> list[Column]:
+                return [
+                    window_function(expr, partition_by, order_by)
+                    for expr in self._call(df)
+                ]
+        else:
+
+            def func(df: SparkLikeLazyFrame) -> list[Column]:
+                return [
+                    expr.over(self._Window.partitionBy(*partition_by))
+                    for expr in self._call(df)
+                ]
 
         return self.__class__(
             func,
@@ -512,6 +557,50 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
 
         return self._from_call(_is_nan, "is_nan")
 
+    def cum_sum(self, *, reverse: bool) -> Self:
+        def func(
+            _input: Column, partition_by: Sequence[str], order_by: Sequence[str]
+        ) -> Column:
+            if reverse:
+                order_by_cols = [self._F.col(x).desc_nulls_last() for x in order_by]
+            else:
+                order_by_cols = [self._F.col(x).asc_nulls_first() for x in order_by]
+            window = (
+                self._Window()
+                .partitionBy(list(partition_by))
+                .orderBy(order_by_cols)
+                .rowsBetween(self._Window().unboundedPreceding, 0)
+            )
+            return self._F.sum(_input).over(window)
+
+        return self._with_window_function(func)
+
+    def rolling_sum(self, window_size: int, *, min_samples: int, center: bool) -> Self:
+        if center:
+            half = (window_size - 1) // 2
+            remainder = (window_size - 1) % 2
+            start = self._Window().currentRow - half - remainder
+            end = self._Window().currentRow + half
+        else:
+            start = self._Window().currentRow - window_size + 1
+            end = self._Window().currentRow
+
+        def func(
+            _input: Column, partition_by: Sequence[str], order_by: Sequence[str]
+        ) -> Column:
+            window = (
+                self._Window()
+                .partitionBy(list(partition_by))
+                .orderBy([self._F.col(x).asc_nulls_first() for x in order_by])
+                .rowsBetween(start, end)
+            )
+            return self._F.when(
+                self._F.count(_input).over(window) >= min_samples,
+                self._F.sum(_input).over(window),
+            )
+
+        return self._with_window_function(func)
+
     @property
     def str(self: Self) -> SparkLikeExprStringNamespace:
         return SparkLikeExprStringNamespace(self)
@@ -539,7 +628,6 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
     sample = not_implemented()
     map_batches = not_implemented()
     ewm_mean = not_implemented()
-    rolling_sum = not_implemented()
     rolling_mean = not_implemented()
     rolling_var = not_implemented()
     rolling_std = not_implemented()
@@ -550,7 +638,6 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
     shift = not_implemented()
     is_first_distinct = not_implemented()
     is_last_distinct = not_implemented()
-    cum_sum = not_implemented()
     cum_count = not_implemented()
     cum_min = not_implemented()
     cum_max = not_implemented()
diff --git a/narwhals/_spark_like/expr_str.py b/narwhals/_spark_like/expr_str.py
index c84eefec..d8646d4d 100644
--- a/narwhals/_spark_like/expr_str.py
+++ b/narwhals/_spark_like/expr_str.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
+from functools import partial
 from typing import TYPE_CHECKING
-from typing import overload
 
 if TYPE_CHECKING:
     from pyspark.sql import Column
@@ -107,47 +107,29 @@ class SparkLikeExprStringNamespace:
         )
 
     def to_datetime(self: Self, format: str | None) -> SparkLikeExpr:  # noqa: A002
-        is_naive = (
-            format is not None
-            and "%s" not in format
-            and "%z" not in format
-            and "Z" not in format
-        )
-        function = (
-            self._compliant_expr._F.to_timestamp_ntz
-            if is_naive
-            else self._compliant_expr._F.to_timestamp
-        )
-        pyspark_format = strptime_to_pyspark_format(format)
-        format = (
-            self._compliant_expr._F.lit(pyspark_format) if is_naive else pyspark_format
-        )
+        F = self._compliant_expr._F  # noqa: N806
+        if not format:
+            function = F.to_timestamp
+        elif is_naive_format(format):
+            function = partial(
+                F.to_timestamp_ntz, format=F.lit(strptime_to_pyspark_format(format))
+            )
+        else:
+            format = strptime_to_pyspark_format(format)
+            function = partial(F.to_timestamp, format=format)
         return self._compliant_expr._from_call(
-            lambda _input: function(
-                self._compliant_expr._F.replace(
-                    _input,
-                    self._compliant_expr._F.lit("T"),
-                    self._compliant_expr._F.lit(" "),
-                ),
-                format=format,
-            ),
+            lambda _input: function(F.replace(_input, F.lit("T"), F.lit(" "))),
             "to_datetime",
         )
 
 
-@overload
-def strptime_to_pyspark_format(format: None) -> None: ...
-
-
-@overload
-def strptime_to_pyspark_format(format: str) -> str: ...
+def is_naive_format(format: str) -> bool:  # noqa: A002
+    return not any(x in format for x in ("%s", "%z", "Z"))
 
 
-def strptime_to_pyspark_format(format: str | None) -> str | None:  # noqa: A002
+def strptime_to_pyspark_format(format: str) -> str:  # noqa: A002
     """Converts a Python strptime datetime format string to a PySpark datetime format string."""
     # Mapping from Python strptime format to PySpark format
-    if format is None:
-        return None
 
     # see https://spark.apache.org/docs/latest/sql-ref-datetime-pattern.html
     # and https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
diff --git a/narwhals/_spark_like/typing.py b/narwhals/_spark_like/typing.py
new file mode 100644
index 00000000..b55ef935
--- /dev/null
+++ b/narwhals/_spark_like/typing.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing import Protocol
+from typing import Sequence
+
+if TYPE_CHECKING:
+    from pyspark.sql import Column
+
+    class WindowFunction(Protocol):
+        def __call__(
+            self, _input: Column, partition_by: Sequence[str], order_by: Sequence[str]
+        ) -> Column: ...
diff --git a/narwhals/_spark_like/utils.py b/narwhals/_spark_like/utils.py
index 6332f35d..c02a100c 100644
--- a/narwhals/_spark_like/utils.py
+++ b/narwhals/_spark_like/utils.py
@@ -55,6 +55,8 @@ def native_to_narwhals_dtype(
     if isinstance(dtype, native.TimestampNTZType):
         return dtypes.Datetime()
     if isinstance(dtype, native.TimestampType):
+        # TODO(marco): is UTC correct, or should we be getting the connection timezone?
+        # https://github.com/narwhals-dev/narwhals/issues/2165
         return dtypes.Datetime(time_zone="UTC")
     if isinstance(dtype, native.DecimalType):
         return dtypes.Decimal()
diff --git a/narwhals/expr.py b/narwhals/expr.py
index b829d378..d7aae157 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -1551,15 +1551,22 @@ class Expr:
         if self._metadata.kind.is_filtration():
             msg = "`.over()` can not be used for expressions which change length."
             raise LengthChangingExprError(msg)
+
+        flat_partition_by = flatten(partition_by)
+        order_by = [_order_by] if isinstance(_order_by, str) else _order_by
+        if not flat_partition_by and not _order_by:  # pragma: no cover
+            msg = "At least one of `partition_by` or `order_by` must be specified."
+            raise ValueError(msg)
+
         kind = ExprKind.TRANSFORM
         n_open_windows = self._metadata.n_open_windows
         if _order_by is not None and self._metadata.kind.is_window():
             n_open_windows -= 1
         metadata = ExprMetadata(kind, n_open_windows=n_open_windows)
-        flat_partition_by = flatten(partition_by)
+
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).over(
-                flat_partition_by, kind=self._metadata.kind
+                flat_partition_by, order_by=order_by, kind=self._metadata.kind
             ),
             metadata,
         )
@@ -2184,14 +2191,14 @@ class Expr:
             |3  4.0            6.0|
             └─────────────────────┘
         """
-        window_size, min_samples = _validate_rolling_arguments(
+        window_size, min_samples_int = _validate_rolling_arguments(
             window_size=window_size, min_samples=min_samples
         )
 
         return self.__class__(
             lambda plx: self._to_compliant_expr(plx).rolling_sum(
                 window_size=window_size,
-                min_samples=min_samples,
+                min_samples=min_samples_int,
                 center=center,
             ),
             self._metadata.with_kind_and_extra_open_window(ExprKind.WINDOW),
diff --git a/narwhals/expr_str.py b/narwhals/expr_str.py
index 92060096..3b837779 100644
--- a/narwhals/expr_str.py
+++ b/narwhals/expr_str.py
@@ -370,6 +370,13 @@ class ExprStringNamespace(Generic[ExprT]):
     def to_datetime(self: Self, format: str | None = None) -> ExprT:  # noqa: A002
         """Convert to Datetime dtype.
 
+        Notes:
+            - pandas defaults to nanosecond time unit, Polars to microsecond.
+              Prior to pandas 2.0, nanoseconds were the only time unit supported
+              in pandas, with no ability to set any other one. The ability to
+              set the time unit in pandas, if the version permits, will arrive.
+            - timezone-aware strings are all converted to and parsed as UTC.
+
         Warning:
             As different backends auto-infer format in different ways, if `format=None`
             there is no guarantee that the result will be equal.
@@ -381,12 +388,6 @@ class ExprStringNamespace(Generic[ExprT]):
         Returns:
             A new expression.
 
-        Notes:
-            pandas defaults to nanosecond time unit, Polars to microsecond.
-            Prior to pandas 2.0, nanoseconds were the only time unit supported
-            in pandas, with no ability to set any other one. The ability to
-            set the time unit in pandas, if the version permits, will arrive.
-
         Examples:
             >>> import polars as pl
             >>> import narwhals as nw
diff --git a/narwhals/group_by.py b/narwhals/group_by.py
index ed86aa46..bc8e626e 100644
--- a/narwhals/group_by.py
+++ b/narwhals/group_by.py
@@ -46,69 +46,32 @@ class GroupBy(Generic[DataFrameT]):
             the grouped sum of another column.
 
             >>> import pandas as pd
-            >>> import polars as pl
             >>> import narwhals as nw
-            >>> df_pd = pd.DataFrame(
-            ...     {
-            ...         "a": ["a", "b", "a", "b", "c"],
-            ...         "b": [1, 2, 1, 3, 3],
-            ...         "c": [5, 4, 3, 2, 1],
-            ...     }
-            ... )
-            >>> df_pl = pl.DataFrame(
+            >>> df_native = pd.DataFrame(
             ...     {
             ...         "a": ["a", "b", "a", "b", "c"],
             ...         "b": [1, 2, 1, 3, 3],
             ...         "c": [5, 4, 3, 2, 1],
             ...     }
             ... )
-
-            We define library agnostic functions:
-
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.group_by("a").agg(nw.col("b").sum()).sort("a")
-
-            >>> @nw.narwhalify
-            ... def func_mult_col(df):
-            ...     return df.group_by("a", "b").agg(nw.sum("c")).sort("a", "b")
-
-            We can then pass either pandas or Polars to `func` and `func_mult_col`:
-
-            >>> func(df_pd)
-               a  b
-            0  a  2
-            1  b  5
-            2  c  3
-            >>> func(df_pl)
-            shape: (3, 2)
-            ┌─────┬─────┐
-            │ a   ┆ b   │
-            │ --- ┆ --- │
-            │ str ┆ i64 │
-            ╞═════╪═════╡
-            │ a   ┆ 2   │
-            │ b   ┆ 5   │
-            │ c   ┆ 3   │
-            └─────┴─────┘
-            >>> func_mult_col(df_pd)
+            >>> df = nw.from_native(df_native)
+            >>>
+            >>> df.group_by("a").agg(nw.col("b").sum()).sort("a")
+            ┌──────────────────┐
+            |Narwhals DataFrame|
+            |------------------|
+            |        a  b      |
+            |     0  a  2      |
+            |     1  b  5      |
+            |     2  c  3      |
+            └──────────────────┘
+            >>>
+            >>> df.group_by("a", "b").agg(nw.col("c").sum()).sort("a", "b").to_native()
                a  b  c
             0  a  1  8
             1  b  2  4
             2  b  3  2
             3  c  3  1
-            >>> func_mult_col(df_pl)
-            shape: (4, 3)
-            ┌─────┬─────┬─────┐
-            │ a   ┆ b   ┆ c   │
-            │ --- ┆ --- ┆ --- │
-            │ str ┆ i64 ┆ i64 │
-            ╞═════╪═════╪═════╡
-            │ a   ┆ 1   ┆ 8   │
-            │ b   ┆ 2   ┆ 4   │
-            │ b   ┆ 3   ┆ 2   │
-            │ c   ┆ 3   ┆ 1   │
-            └─────┴─────┴─────┘
         """
         flat_aggs = tuple(flatten(aggs))
         if not all_exprs_are_scalar_like(*flat_aggs, **named_aggs):
@@ -162,29 +125,16 @@ class LazyGroupBy(Generic[LazyFrameT]):
             >>> import polars as pl
             >>> import narwhals as nw
             >>> from narwhals.typing import IntoFrameT
-            >>> lf_pl = pl.LazyFrame(
+            >>> lf_native = pl.LazyFrame(
             ...     {
             ...         "a": ["a", "b", "a", "b", "c"],
             ...         "b": [1, 2, 1, 3, 3],
             ...         "c": [5, 4, 3, 2, 1],
             ...     }
             ... )
-
-            We define library agnostic functions:
-
-            >>> def agnostic_func_one_col(lf_native: IntoFrameT) -> IntoFrameT:
-            ...     lf = nw.from_native(lf_native)
-            ...     return nw.to_native(lf.group_by("a").agg(nw.col("b").sum()).sort("a"))
-
-            >>> def agnostic_func_mult_col(lf_native: IntoFrameT) -> IntoFrameT:
-            ...     lf = nw.from_native(lf_native)
-            ...     return nw.to_native(
-            ...         lf.group_by("a", "b").agg(nw.sum("c")).sort("a", "b")
-            ...     )
-
-            We can then pass a lazy frame and materialise it with `collect`:
-
-            >>> agnostic_func_one_col(lf_pl).collect()
+            >>> lf = nw.from_native(lf_native)
+            >>>
+            >>> nw.to_native(lf.group_by("a").agg(nw.col("b").sum()).sort("a")).collect()
             shape: (3, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -195,18 +145,23 @@ class LazyGroupBy(Generic[LazyFrameT]):
             │ b   ┆ 5   │
             │ c   ┆ 3   │
             └─────┴─────┘
-            >>> agnostic_func_mult_col(lf_pl).collect()
-            shape: (4, 3)
-            ┌─────┬─────┬─────┐
-            │ a   ┆ b   ┆ c   │
-            │ --- ┆ --- ┆ --- │
-            │ str ┆ i64 ┆ i64 │
-            ╞═════╪═════╪═════╡
-            │ a   ┆ 1   ┆ 8   │
-            │ b   ┆ 2   ┆ 4   │
-            │ b   ┆ 3   ┆ 2   │
-            │ c   ┆ 3   ┆ 1   │
-            └─────┴─────┴─────┘
+            >>>
+            >>> lf.group_by("a", "b").agg(nw.sum("c")).sort("a", "b").collect()
+            ┌───────────────────┐
+            |Narwhals DataFrame |
+            |-------------------|
+            |shape: (4, 3)      |
+            |┌─────┬─────┬─────┐|
+            |│ a   ┆ b   ┆ c   │|
+            |│ --- ┆ --- ┆ --- │|
+            |│ str ┆ i64 ┆ i64 │|
+            |╞═════╪═════╪═════╡|
+            |│ a   ┆ 1   ┆ 8   │|
+            |│ b   ┆ 2   ┆ 4   │|
+            |│ b   ┆ 3   ┆ 2   │|
+            |│ c   ┆ 3   ┆ 1   │|
+            |└─────┴─────┴─────┘|
+            └───────────────────┘
         """
         flat_aggs = tuple(flatten(aggs))
         if not all_exprs_are_scalar_like(*flat_aggs, **named_aggs):
diff --git a/narwhals/selectors.py b/narwhals/selectors.py
index 6866a5fd..f2293ebd 100644
--- a/narwhals/selectors.py
+++ b/narwhals/selectors.py
@@ -48,45 +48,15 @@ def by_dtype(*dtypes: DType | type[DType] | Iterable[DType | type[DType]]) -> Se
         A new expression.
 
     Examples:
-        >>> import pandas as pd
-        >>> import polars as pl
         >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> import narwhals.selectors as ncs
-        >>> from narwhals.typing import IntoFrameT
-        >>>
-        >>> data = {"a": [1, 2], "b": ["x", "y"], "c": [4.1, 2.3]}
-        >>> df_pd = pd.DataFrame(data)
-        >>> df_pl = pl.DataFrame(data)
-        >>> df_pa = pa.table(data)
-
-        Let's define a dataframe-agnostic function to select int64 and float64
-        dtypes and multiplies each value by 2:
-
-        >>> def agnostic_select_by_dtype(df_native: IntoFrameT) -> IntoFrameT:
-        ...     df_nw = nw.from_native(df_native)
-        ...     return df_nw.select(ncs.by_dtype(nw.Int64, nw.Float64) * 2).to_native()
-
-        We can then pass any supported library such as pandas, Polars, or
-        PyArrow to `agnostic_select_by_dtype`:
+        >>> df_native = pa.table({"a": [1, 2], "b": ["x", "y"], "c": [4.1, 2.3]})
+        >>> df = nw.from_native(df_native)
 
-        >>> agnostic_select_by_dtype(df_pd)
-           a    c
-        0  2  8.2
-        1  4  4.6
-
-        >>> agnostic_select_by_dtype(df_pl)
-        shape: (2, 2)
-        ┌─────┬─────┐
-        │ a   ┆ c   │
-        │ --- ┆ --- │
-        │ i64 ┆ f64 │
-        ╞═════╪═════╡
-        │ 2   ┆ 8.2 │
-        │ 4   ┆ 4.6 │
-        └─────┴─────┘
+        Let's select int64 and float64  dtypes and multiply each value by 2:
 
-        >>> agnostic_select_by_dtype(df_pa)
+        >>> df.select(ncs.by_dtype(nw.Int64, nw.Float64) * 2).to_native()
         pyarrow.Table
         a: int64
         c: double
@@ -111,55 +81,23 @@ def matches(pattern: str) -> Selector:
 
     Examples:
         >>> import pandas as pd
-        >>> import polars as pl
-        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> import narwhals.selectors as ncs
-        >>> from narwhals.typing import IntoFrameT
-        >>>
-        >>> data = {
-        ...     "foo": ["x", "y"],
-        ...     "bar": [123, 456],
-        ...     "baz": [2.0, 5.5],
-        ...     "zap": [0, 1],
-        ... }
-        >>> df_pd = pd.DataFrame(data)
-        >>> df_pl = pl.DataFrame(data)
-        >>> df_pa = pa.table(data)
-
-        Let's define a dataframe-agnostic function to select column names
-        containing an 'a', preceded by a character that is not 'z':
-
-        >>> def agnostic_select_match(df_native: IntoFrameT) -> IntoFrameT:
-        ...     df_nw = nw.from_native(df_native)
-        ...     return df_nw.select(ncs.matches("[^z]a")).to_native()
-
-        We can then pass any supported library such as pandas, Polars, or
-        PyArrow to `agnostic_select_match`:
-
-        >>> agnostic_select_match(df_pd)
+        >>> df_native = pd.DataFrame(
+        ...     {
+        ...         "bar": [123, 456],
+        ...         "baz": [2.0, 5.5],
+        ...         "zap": [0, 1],
+        ...     }
+        ... )
+        >>> df = nw.from_native(df_native)
+
+        Let's select column names containing an 'a', preceded by a character that is not 'z':
+
+        >>> df.select(ncs.matches("[^z]a")).to_native()
            bar  baz
         0  123  2.0
         1  456  5.5
-
-        >>> agnostic_select_match(df_pl)
-        shape: (2, 2)
-        ┌─────┬─────┐
-        │ bar ┆ baz │
-        │ --- ┆ --- │
-        │ i64 ┆ f64 │
-        ╞═════╪═════╡
-        │ 123 ┆ 2.0 │
-        │ 456 ┆ 5.5 │
-        └─────┴─────┘
-
-        >>> agnostic_select_match(df_pa)
-        pyarrow.Table
-        bar: int64
-        baz: double
-        ----
-        bar: [[123,456]]
-        baz: [[2,5.5]]
     """
     return Selector(lambda plx: plx.selectors.matches(pattern), ExprMetadata.selector())
 
@@ -171,34 +109,15 @@ def numeric() -> Selector:
         A new expression.
 
     Examples:
-        >>> import pandas as pd
         >>> import polars as pl
-        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> import narwhals.selectors as ncs
-        >>> from narwhals.typing import IntoFrameT
-        >>>
-        >>> data = {"a": [1, 2], "b": ["x", "y"], "c": [4.1, 2.3]}
-        >>> df_pd = pd.DataFrame(data)
-        >>> df_pl = pl.DataFrame(data)
-        >>> df_pa = pa.table(data)
+        >>> df_native = pl.DataFrame({"a": [1, 2], "b": ["x", "y"], "c": [4.1, 2.3]})
+        >>> df = nw.from_native(df_native)
 
-        Let's define a dataframe-agnostic function to select numeric
-        dtypes and multiplies each value by 2:
+        Let's select numeric dtypes and multiply each value by 2:
 
-        >>> def agnostic_select_numeric(df_native: IntoFrameT) -> IntoFrameT:
-        ...     df_nw = nw.from_native(df_native)
-        ...     return df_nw.select(ncs.numeric() * 2).to_native()
-
-        We can then pass any supported library such as pandas, Polars, or
-        PyArrow to `agnostic_select_numeric`:
-
-        >>> agnostic_select_numeric(df_pd)
-           a    c
-        0  2  8.2
-        1  4  4.6
-
-        >>> agnostic_select_numeric(df_pl)
+        >>> df.select(ncs.numeric() * 2).to_native()
         shape: (2, 2)
         ┌─────┬─────┐
         │ a   ┆ c   │
@@ -208,14 +127,6 @@ def numeric() -> Selector:
         │ 2   ┆ 8.2 │
         │ 4   ┆ 4.6 │
         └─────┴─────┘
-
-        >>> agnostic_select_numeric(df_pa)
-        pyarrow.Table
-        a: int64
-        c: double
-        ----
-        a: [[2,4]]
-        c: [[8.2,4.6]]
     """
     return Selector(lambda plx: plx.selectors.numeric(), ExprMetadata.selector())
 
@@ -227,48 +138,28 @@ def boolean() -> Selector:
         A new expression.
 
     Examples:
-        >>> import pandas as pd
         >>> import polars as pl
-        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> import narwhals.selectors as ncs
-        >>> from narwhals.typing import IntoFrameT
-        >>>
-        >>> data = {"a": [1, 2], "b": ["x", "y"], "c": [False, True]}
-        >>> df_pd = pd.DataFrame(data)
-        >>> df_pl = pl.DataFrame(data)
-        >>> df_pa = pa.table(data)
-
-        Let's define a dataframe-agnostic function to select boolean dtypes:
-
-        >>> def agnostic_select_boolean(df_native: IntoFrameT) -> IntoFrameT:
-        ...     df_nw = nw.from_native(df_native)
-        ...     return df_nw.select(ncs.boolean()).to_native()
-
-        We can then pass any supported library such as pandas, Polars, or
-        PyArrow to `agnostic_select_boolean`:
-
-        >>> agnostic_select_boolean(df_pd)
-               c
-        0  False
-        1   True
-
-        >>> agnostic_select_boolean(df_pl)
-        shape: (2, 1)
-        ┌───────┐
-        │ c     │
-        │ ---   │
-        │ bool  │
-        ╞═══════╡
-        │ false │
-        │ true  │
-        └───────┘
-
-        >>> agnostic_select_boolean(df_pa)
-        pyarrow.Table
-        c: bool
-        ----
-        c: [[false,true]]
+        >>> df_native = pl.DataFrame({"a": [1, 2], "b": ["x", "y"], "c": [False, True]})
+        >>> df = nw.from_native(df_native)
+
+        Let's select boolean dtypes:
+
+        >>> df.select(ncs.boolean())
+        ┌──────────────────┐
+        |Narwhals DataFrame|
+        |------------------|
+        |  shape: (2, 1)   |
+        |  ┌───────┐       |
+        |  │ c     │       |
+        |  │ ---   │       |
+        |  │ bool  │       |
+        |  ╞═══════╡       |
+        |  │ false │       |
+        |  │ true  │       |
+        |  └───────┘       |
+        └──────────────────┘
     """
     return Selector(lambda plx: plx.selectors.boolean(), ExprMetadata.selector())
 
@@ -280,33 +171,15 @@ def string() -> Selector:
         A new expression.
 
     Examples:
-        >>> import pandas as pd
         >>> import polars as pl
-        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> import narwhals.selectors as ncs
-        >>> from narwhals.typing import IntoFrameT
-        >>>
-        >>> data = {"a": [1, 2], "b": ["x", "y"], "c": [False, True]}
-        >>> df_pd = pd.DataFrame(data)
-        >>> df_pl = pl.DataFrame(data)
-        >>> df_pa = pa.table(data)
-
-        Let's define a dataframe-agnostic function to select string dtypes:
-
-        >>> def agnostic_select_string(df_native: IntoFrameT) -> IntoFrameT:
-        ...     df_nw = nw.from_native(df_native)
-        ...     return df_nw.select(ncs.string()).to_native()
-
-        We can then pass any supported library such as pandas, Polars, or
-        PyArrow to `agnostic_select_string`:
+        >>> df_native = pl.DataFrame({"a": [1, 2], "b": ["x", "y"], "c": [False, True]})
+        >>> df = nw.from_native(df_native)
 
-        >>> agnostic_select_string(df_pd)
-           b
-        0  x
-        1  y
+        Let's select string dtypes:
 
-        >>> agnostic_select_string(df_pl)
+        >>> df.select(ncs.string()).to_native()
         shape: (2, 1)
         ┌─────┐
         │ b   │
@@ -316,12 +189,6 @@ def string() -> Selector:
         │ x   │
         │ y   │
         └─────┘
-
-        >>> agnostic_select_string(df_pa)
-        pyarrow.Table
-        b: string
-        ----
-        b: [["x","y"]]
     """
     return Selector(lambda plx: plx.selectors.string(), ExprMetadata.selector())
 
@@ -333,36 +200,17 @@ def categorical() -> Selector:
         A new expression.
 
     Examples:
-        >>> import pandas as pd
         >>> import polars as pl
-        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> import narwhals.selectors as ncs
-        >>> from narwhals.typing import IntoFrameT
-        >>>
-        >>> data = {"a": [1, 2], "b": ["x", "y"], "c": [False, True]}
-        >>> df_pd = pd.DataFrame(data)
-        >>> df_pl = pl.DataFrame(data)
-        >>> df_pa = pa.table(data)
+        >>> df_native = pl.DataFrame({"a": [1, 2], "b": ["x", "y"], "c": [False, True]})
 
-        Let's define a dataframe-agnostic function that first converts column "b" to
-        categorical, and then selects categorical dtypes:
+        Let's convert column "b" to categorical, and then select categorical dtypes:
 
-        >>> def agnostic_select_categorical(df_native: IntoFrameT) -> IntoFrameT:
-        ...     df_nw = nw.from_native(df_native).with_columns(
-        ...         b=nw.col("b").cast(nw.Categorical())
-        ...     )
-        ...     return df_nw.select(ncs.categorical()).to_native()
-
-        We can then pass any supported library such as pandas, Polars, or
-        PyArrow to `agnostic_select_categorical`:
-
-        >>> agnostic_select_categorical(df_pd)
-           b
-        0  x
-        1  y
-
-        >>> agnostic_select_categorical(df_pl)
+        >>> df = nw.from_native(df_native).with_columns(
+        ...     b=nw.col("b").cast(nw.Categorical())
+        ... )
+        >>> df.select(ncs.categorical()).to_native()
         shape: (2, 1)
         ┌─────┐
         │ b   │
@@ -372,14 +220,6 @@ def categorical() -> Selector:
         │ x   │
         │ y   │
         └─────┘
-
-        >>> agnostic_select_categorical(df_pa)
-        pyarrow.Table
-        b: dictionary<values=string, indices=uint32, ordered=0>
-        ----
-        b: [  -- dictionary:
-        ["x","y"]  -- indices:
-        [0,1]]
     """
     return Selector(lambda plx: plx.selectors.categorical(), ExprMetadata.selector())
 
@@ -392,51 +232,17 @@ def all() -> Selector:
 
     Examples:
         >>> import pandas as pd
-        >>> import polars as pl
-        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> import narwhals.selectors as ncs
-        >>> from narwhals.typing import IntoFrameT
-        >>>
-        >>> data = {"a": [1, 2], "b": ["x", "y"], "c": [False, True]}
-        >>> df_pd = pd.DataFrame(data)
-        >>> df_pl = pl.DataFrame(data)
-        >>> df_pa = pa.table(data)
-
-        Let's define a dataframe-agnostic function to select all dtypes:
+        >>> df_native = pd.DataFrame({"a": [1, 2], "b": ["x", "y"], "c": [False, True]})
+        >>> df = nw.from_native(df_native)
 
-        >>> def agnostic_select_all(df_native: IntoFrameT) -> IntoFrameT:
-        ...     df_nw = nw.from_native(df_native)
-        ...     return df_nw.select(ncs.all()).to_native()
+        Let's select all dtypes:
 
-        We can then pass any supported library such as pandas, Polars, or
-        PyArrow to `agnostic_select_all`:
-
-        >>> agnostic_select_all(df_pd)
+        >>> df.select(ncs.all()).to_native()
            a  b      c
         0  1  x  False
         1  2  y   True
-
-        >>> agnostic_select_all(df_pl)
-        shape: (2, 3)
-        ┌─────┬─────┬───────┐
-        │ a   ┆ b   ┆ c     │
-        │ --- ┆ --- ┆ ---   │
-        │ i64 ┆ str ┆ bool  │
-        ╞═════╪═════╪═══════╡
-        │ 1   ┆ x   ┆ false │
-        │ 2   ┆ y   ┆ true  │
-        └─────┴─────┴───────┘
-
-        >>> agnostic_select_all(df_pa)
-        pyarrow.Table
-        a: int64
-        b: string
-        c: bool
-        ----
-        a: [[1,2]]
-        b: [["x","y"]]
-        c: [[false,true]]
     """
     return Selector(lambda plx: plx.selectors.all(), ExprMetadata.selector())
 
diff --git a/narwhals/series.py b/narwhals/series.py
index 464b7086..749b5d1a 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -1578,7 +1578,7 @@ class Series(Generic[IntoSeriesT]):
     def __invert__(self: Self) -> Self:
         return self._from_compliant_series(self._compliant_series.__invert__())
 
-    def filter(self: Self, other: Any) -> Self:
+    def filter(self: Self, predicate: Any) -> Self:
         """Filter elements in the Series based on a condition.
 
         Returns:
@@ -1597,7 +1597,7 @@ class Series(Generic[IntoSeriesT]):
             dtype: int64
         """
         return self._from_compliant_series(
-            self._compliant_series.filter(self._extract_native(other))
+            self._compliant_series.filter(self._extract_native(predicate))
         )
 
     # --- descriptive ---
@@ -2287,7 +2287,7 @@ class Series(Generic[IntoSeriesT]):
             3    7.0
             dtype: float64
         """
-        window_size, min_samples = _validate_rolling_arguments(
+        window_size, min_samples_int = _validate_rolling_arguments(
             window_size=window_size, min_samples=min_samples
         )
 
@@ -2297,7 +2297,7 @@ class Series(Generic[IntoSeriesT]):
         return self._from_compliant_series(
             self._compliant_series.rolling_sum(
                 window_size=window_size,
-                min_samples=min_samples,
+                min_samples=min_samples_int,
                 center=center,
             )
         )
diff --git a/narwhals/series_cat.py b/narwhals/series_cat.py
index 73f899d1..54634677 100644
--- a/narwhals/series_cat.py
+++ b/narwhals/series_cat.py
@@ -24,50 +24,14 @@ class SeriesCatNamespace(Generic[SeriesT]):
             A new Series containing the unique categories.
 
         Examples:
-            Let's create some series:
-
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = ["apple", "mango", "mango"]
-            >>> s_pd = pd.Series(data, dtype="category")
-            >>> s_pl = pl.Series(data, dtype=pl.Categorical)
-            >>> s_pa = pa.chunked_array([data]).dictionary_encode()
-
-            We define a dataframe-agnostic function to get unique categories
-            from column 'fruits':
-
-            >>> def agnostic_get_categories(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.cat.get_categories().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_get_categories`:
-
-            >>> agnostic_get_categories(s_pd)
+            >>> s_native = pd.Series(["apple", "mango", "mango"], dtype="category")
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.cat.get_categories().to_native()
             0    apple
             1    mango
             dtype: object
-
-            >>> agnostic_get_categories(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [str]
-            [
-               "apple"
-               "mango"
-            ]
-
-            >>> agnostic_get_categories(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                "apple",
-                "mango"
-              ]
-            ]
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.cat.get_categories()
diff --git a/narwhals/series_dt.py b/narwhals/series_dt.py
index 10f53128..6a99aed4 100644
--- a/narwhals/series_dt.py
+++ b/narwhals/series_dt.py
@@ -30,46 +30,15 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Examples:
             >>> from datetime import datetime
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [datetime(2012, 1, 7, 10, 20), datetime(2023, 3, 10, 11, 32)]
-            >>> s_pd = pd.Series(dates).convert_dtypes(dtype_backend="pyarrow")
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_date(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.date().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_date`:
-
-            >>> agnostic_date(s_pd)
+            >>> s_native = pd.Series(
+            ...     [datetime(2012, 1, 7, 10, 20), datetime(2023, 3, 10, 11, 32)]
+            ... ).convert_dtypes(dtype_backend="pyarrow")
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.date().to_native()
             0    2012-01-07
             1    2023-03-10
             dtype: date32[day][pyarrow]
-
-            >>> agnostic_date(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [date]
-            [
-               2012-01-07
-               2023-03-10
-            ]
-
-            >>> agnostic_date(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                2012-01-07,
-                2023-03-10
-              ]
-            ]
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.dt.date()
@@ -83,46 +52,16 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import datetime
-            >>> import pandas as pd
             >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [datetime(2012, 1, 7), datetime(2023, 3, 10)]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_year(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.year().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_year`:
-
-            >>> agnostic_year(s_pd)
-            0    2012
-            1    2023
-            dtype: int...
-
-            >>> agnostic_year(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            >>> s_native = pl.Series([datetime(2012, 1, 7), datetime(2023, 3, 10)])
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.year().to_native()  # doctest: +NORMALIZE_WHITESPACE
             shape: (2,)
             Series: '' [i32]
             [
-               2012
-               2023
-            ]
-
-            >>> agnostic_year(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                2012,
-                2023
-              ]
+                    2012
+                    2023
             ]
         """
         return self._narwhals_series._from_compliant_series(
@@ -137,45 +76,16 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import datetime
-            >>> import pandas as pd
             >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [datetime(2023, 2, 1), datetime(2023, 8, 3)]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_month(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.month().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_month`:
-
-            >>> agnostic_month(s_pd)
-            0    2
-            1    8
-            dtype: int...
-            >>> agnostic_month(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            >>> s_native = pl.Series([datetime(2012, 1, 7), datetime(2023, 3, 10)])
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.month().to_native()  # doctest: +NORMALIZE_WHITESPACE
             shape: (2,)
             Series: '' [i8]
             [
-               2
-               8
-            ]
-
-            >>> agnostic_month(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                2,
-                8
-              ]
+                    1
+                    3
             ]
         """
         return self._narwhals_series._from_compliant_series(
@@ -190,40 +100,13 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import datetime
-            >>> import pandas as pd
-            >>> import polars as pl
             >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [datetime(2022, 1, 1), datetime(2022, 1, 5)]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_day(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.day().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_day`:
-
-            >>> agnostic_day(s_pd)
-            0    1
-            1    5
-            dtype: int...
-
-            >>> agnostic_day(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [i8]
-            [
-               1
-               5
-            ]
-
-            >>> agnostic_day(s_pa)  # doctest: +ELLIPSIS
+            >>> s_native = pa.chunked_array(
+            ...     [[datetime(2022, 1, 1), datetime(2022, 1, 5)]]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.day().to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
               [
@@ -244,40 +127,13 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import datetime
-            >>> import pandas as pd
-            >>> import polars as pl
             >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [datetime(2022, 1, 1, 5, 3), datetime(2022, 1, 5, 9, 12)]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_hour(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.hour().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_hour`:
-
-            >>> agnostic_hour(s_pd)
-            0    5
-            1    9
-            dtype: int...
-
-            >>> agnostic_hour(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [i8]
-            [
-               5
-               9
-            ]
-
-            >>> agnostic_hour(s_pa)  # doctest: +ELLIPSIS
+            >>> s_native = pa.chunked_array(
+            ...     [[datetime(2022, 1, 1, 5, 3), datetime(2022, 1, 5, 9, 12)]]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.hour().to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
               [
@@ -299,46 +155,15 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Examples:
             >>> from datetime import datetime
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [datetime(2022, 1, 1, 5, 3), datetime(2022, 1, 5, 9, 12)]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_minute(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.minute().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_minute`:
-
-            >>> agnostic_minute(s_pd)
+            >>> s_native = pd.Series(
+            ...     [datetime(2022, 1, 1, 5, 3), datetime(2022, 1, 5, 9, 12)]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.minute().to_native()
             0     3
             1    12
-            dtype: int...
-
-            >>> agnostic_minute(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [i8]
-            [
-               3
-               12
-            ]
-
-            >>> agnostic_minute(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                3,
-                12
-              ]
-            ]
+            dtype: int32
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.dt.minute()
@@ -353,46 +178,15 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Examples:
             >>> from datetime import datetime
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [datetime(2022, 1, 1, 5, 3, 10), datetime(2022, 1, 5, 9, 12, 4)]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_second(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.second().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_second`:
-
-            >>> agnostic_second(s_pd)
+            >>> s_native = pd.Series(
+            ...     [datetime(2022, 1, 1, 5, 3, 10), datetime(2022, 1, 5, 9, 12, 4)]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.second().to_native()
             0    10
             1     4
-            dtype: int...
-
-            >>> agnostic_second(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [i8]
-            [
-               10
-                4
-            ]
-
-            >>> agnostic_second(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                10,
-                4
-              ]
-            ]
+            dtype: int32
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.dt.second()
@@ -407,61 +201,18 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Examples:
             >>> from datetime import datetime
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [
-            ...     datetime(2023, 5, 21, 12, 55, 10, 400000),
-            ...     datetime(2023, 5, 21, 12, 55, 10, 600000),
-            ...     datetime(2023, 5, 21, 12, 55, 10, 800000),
-            ...     datetime(2023, 5, 21, 12, 55, 11, 0),
-            ...     datetime(2023, 5, 21, 12, 55, 11, 200000),
-            ... ]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_millisecond(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.millisecond().alias("datetime").to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_millisecond`:
-
-            >>> agnostic_millisecond(s_pd)
+            >>> s_native = pd.Series(
+            ...     [
+            ...         datetime(2022, 1, 1, 5, 3, 7, 400000),
+            ...         datetime(2022, 1, 1, 5, 3, 7, 0),
+            ...     ]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.millisecond().alias("datetime").to_native()
             0    400
-            1    600
-            2    800
-            3      0
-            4    200
-            Name: datetime, dtype: int...
-
-            >>> agnostic_millisecond(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (5,)
-            Series: 'datetime' [i32]
-            [
-                400
-                600
-                800
-                0
-                200
-            ]
-
-            >>> agnostic_millisecond(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                400,
-                600,
-                800,
-                0,
-                200
-              ]
-            ]
+            1      0
+            Name: datetime, dtype: int32
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.dt.millisecond()
@@ -476,61 +227,19 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Examples:
             >>> from datetime import datetime
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [
-            ...     datetime(2023, 5, 21, 12, 55, 10, 400000),
-            ...     datetime(2023, 5, 21, 12, 55, 10, 600000),
-            ...     datetime(2023, 5, 21, 12, 55, 10, 800000),
-            ...     datetime(2023, 5, 21, 12, 55, 11, 0),
-            ...     datetime(2023, 5, 21, 12, 55, 11, 200000),
-            ... ]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_microsecond(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.microsecond().alias("datetime").to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_microsecond`:
-
-            >>> agnostic_microsecond(s_pd)
+            >>> s_native = pd.Series(
+            ...     [
+            ...         datetime(2022, 1, 1, 5, 3, 7, 400000),
+            ...         datetime(2022, 1, 1, 5, 3, 7, 0),
+            ...     ]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.microsecond().alias("datetime").to_native()
             0    400000
-            1    600000
-            2    800000
-            3         0
-            4    200000
-            Name: datetime, dtype: int...
-
-            >>> agnostic_microsecond(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (5,)
-            Series: 'datetime' [i32]
-            [
-               400000
-               600000
-               800000
-               0
-               200000
-            ]
+            1         0
+            Name: datetime, dtype: int32
 
-            >>> agnostic_microsecond(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                400000,
-                600000,
-                800000,
-                0,
-                200000
-              ]
-            ]
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.dt.microsecond()
@@ -545,49 +254,18 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Examples:
             >>> from datetime import datetime
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> dates = [
-            ...     datetime(2022, 1, 1, 5, 3, 10, 500000),
-            ...     datetime(2022, 1, 5, 9, 12, 4, 60000),
-            ... ]
-            >>> s_pd = pd.Series(dates)
-            >>> s_pl = pl.Series(dates)
-            >>> s_pa = pa.chunked_array([dates])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_nanosecond(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.nanosecond().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_nanosecond`:
-
-            >>> agnostic_nanosecond(s_pd)
-            0    500000000
-            1     60000000
-            dtype: int...
-
-            >>> agnostic_nanosecond(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [i32]
-            [
-               500000000
-               60000000
-            ]
-
-            >>> agnostic_nanosecond(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                500000000,
-                60000000
-              ]
-            ]
+            >>> s_native = pd.Series(
+            ...     [
+            ...         datetime(2022, 1, 1, 5, 3, 7, 400000),
+            ...         datetime(2022, 1, 1, 5, 3, 7, 0),
+            ...     ]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.nanosecond().alias("datetime").to_native()
+            0    400000000
+            1            0
+            Name: datetime, dtype: int32
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.dt.nanosecond()
@@ -601,41 +279,13 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import datetime
-            >>> import pandas as pd
-            >>> import polars as pl
             >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [datetime(2020, 1, 1), datetime(2020, 8, 3)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_ordinal_day(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.ordinal_day().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_ordinal_day`:
-
-            >>> agnostic_ordinal_day(s_pd)
-            0      1
-            1    216
-            dtype: int32
-
-            >>> agnostic_ordinal_day(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [i16]
-            [
-               1
-               216
-            ]
-
-
-            >>> agnostic_ordinal_day(s_pa)  # doctest: +ELLIPSIS
+            >>> s_native = pa.chunked_array(
+            ...     [[datetime(2020, 1, 1), datetime(2020, 8, 3)]]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.ordinal_day().to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
               [
@@ -655,39 +305,15 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
             A new Series containing the week day for each datetime value.
             Returns the ISO weekday number where monday = 1 and sunday = 7
 
-
         Examples:
             >>> from datetime import datetime
-            >>> import pandas as pd
-            >>> import polars as pl
             >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-            >>> data = [datetime(2020, 1, 1), datetime(2020, 8, 3)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_weekday(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.weekday().to_native()
-
-            We can then pass either pandas, Polars, PyArrow, and other supported libraries to `agnostic_weekday`:
-
-            >>> agnostic_weekday(s_pd)
-            0    3
-            1    1
-            dtype: int32
-            >>> agnostic_weekday(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [i8]
-            [
-               3
-               1
-            ]
-            >>> agnostic_weekday(s_pa)  # doctest: +ELLIPSIS
+            >>> s_native = pa.chunked_array(
+            ...     [[datetime(2020, 1, 1), datetime(2020, 8, 3)]]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.weekday().to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
               [
@@ -713,47 +339,19 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import timedelta
-            >>> import pandas as pd
             >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [timedelta(minutes=10), timedelta(minutes=20, seconds=40)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_total_minutes(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.total_minutes().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_total_minutes`:
-
-            >>> agnostic_total_minutes(s_pd)
-            0    10
-            1    20
-            dtype: int...
-
-            >>> agnostic_total_minutes(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            >>> s_native = pl.Series(
+            ...     [timedelta(minutes=10), timedelta(minutes=20, seconds=40)]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.total_minutes().to_native()  # doctest: +NORMALIZE_WHITESPACE
             shape: (2,)
             Series: '' [i64]
             [
                     10
                     20
             ]
-
-            >>> agnostic_total_minutes(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                10,
-                20
-              ]
-            ]
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.dt.total_minutes()
@@ -772,46 +370,18 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import timedelta
-            >>> import pandas as pd
             >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [timedelta(seconds=10), timedelta(seconds=20, milliseconds=40)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_total_seconds(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.total_seconds().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_total_seconds`:
-
-            >>> agnostic_total_seconds(s_pd)
-            0    10
-            1    20
-            dtype: int...
-
-            >>> agnostic_total_seconds(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            >>> s_native = pl.Series(
+            ...     [timedelta(minutes=10), timedelta(minutes=20, seconds=40)]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.total_seconds().to_native()  # doctest: +NORMALIZE_WHITESPACE
             shape: (2,)
             Series: '' [i64]
             [
-                    10
-                    20
-            ]
-
-            >>> agnostic_total_seconds(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                10,
-                20
-              ]
+                    600
+                    1240
             ]
         """
         return self._narwhals_series._from_compliant_series(
@@ -831,50 +401,22 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import timedelta
-            >>> import pandas as pd
             >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [
-            ...     timedelta(milliseconds=10),
-            ...     timedelta(milliseconds=20, microseconds=40),
-            ... ]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_total_milliseconds(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.total_milliseconds().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_total_milliseconds`:
-
-            >>> agnostic_total_milliseconds(s_pd)
-            0    10
-            1    20
-            dtype: int...
-
-            >>> agnostic_total_milliseconds(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            >>> s_native = pl.Series(
+            ...     [
+            ...         timedelta(milliseconds=10),
+            ...         timedelta(milliseconds=20, microseconds=40),
+            ...     ]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.total_milliseconds().to_native()  # doctest: +NORMALIZE_WHITESPACE
             shape: (2,)
             Series: '' [i64]
             [
                     10
                     20
             ]
-
-            >>> agnostic_total_milliseconds(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                10,
-                20
-              ]
-            ]
         """
         return self._narwhals_series._from_compliant_series(
             self._narwhals_series._compliant_series.dt.total_milliseconds()
@@ -893,49 +435,21 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import timedelta
-            >>> import pandas as pd
             >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [
-            ...     timedelta(microseconds=10),
-            ...     timedelta(milliseconds=1, microseconds=200),
-            ... ]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            We define a library agnostic function:
-
-            >>> def agnostic_total_microseconds(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.total_microseconds().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_total_microseconds`:
-
-            >>> agnostic_total_microseconds(s_pd)
-            0      10
-            1    1200
-            dtype: int...
-
-            >>> agnostic_total_microseconds(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            >>> s_native = pl.Series(
+            ...     [
+            ...         timedelta(microseconds=10),
+            ...         timedelta(milliseconds=1, microseconds=200),
+            ...     ]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.total_microseconds().to_native()  # doctest: +NORMALIZE_WHITESPACE
             shape: (2,)
             Series: '' [i64]
             [
-                10
-                1200
-            ]
-
-            >>> agnostic_total_microseconds(s_pa)  # doctest: +ELLIPSIS
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                10,
-                1200
-              ]
+                    10
+                    1200
             ]
         """
         return self._narwhals_series._from_compliant_series(
@@ -955,31 +469,13 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import datetime
-            >>> import pandas as pd
             >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = ["2024-01-01 00:00:00.000000001", "2024-01-01 00:00:00.000000002"]
-            >>> s_pd = pd.to_datetime(pd.Series(data))
-            >>> s_pl = pl.Series(data).str.to_datetime(time_unit="ns")
-
-            We define a library agnostic function:
-
-            >>> def agnostic_total_nanoseconds(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.diff().dt.total_nanoseconds().to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_total_nanoseconds`:
-
-            >>> agnostic_total_nanoseconds(s_pd)
-            0    NaN
-            1    1.0
-            dtype: float64
-
-            >>> agnostic_total_nanoseconds(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            >>> s_native = pl.Series(
+            ...     ["2024-01-01 00:00:00.000000001", "2024-01-01 00:00:00.000000002"]
+            ... ).str.to_datetime(time_unit="ns")
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.diff().dt.total_nanoseconds().to_native()  # doctest: +NORMALIZE_WHITESPACE
             shape: (2,)
             Series: '' [i64]
             [
@@ -1033,52 +529,23 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import datetime
-            >>> import pandas as pd
-            >>> import polars as pl
             >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [
-            ...     datetime(2020, 3, 1),
-            ...     datetime(2020, 4, 1),
-            ...     datetime(2020, 5, 1),
-            ... ]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            We define a dataframe-agnostic function:
-
-            >>> def agnostic_to_string(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.to_string("%Y/%m/%d").to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_to_string`:
-
-            >>> agnostic_to_string(s_pd)
-            0    2020/03/01
-            1    2020/04/01
-            2    2020/05/01
-            dtype: object
-
-            >>> agnostic_to_string(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (3,)
-            Series: '' [str]
-            [
-               "2020/03/01"
-               "2020/04/01"
-               "2020/05/01"
-            ]
-
-            >>> agnostic_to_string(s_pa)  # doctest: +ELLIPSIS
+            >>> s_native = pa.chunked_array(
+            ...     [
+            ...         [
+            ...             datetime(2020, 3, 1),
+            ...             datetime(2020, 4, 1),
+            ...         ]
+            ...     ]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.to_string("%Y/%m/%d").to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
               [
                 "2020/03/01",
-                "2020/04/01",
-                "2020/05/01"
+                "2020/04/01"
               ]
             ]
         """
@@ -1097,49 +564,23 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
 
         Examples:
             >>> from datetime import datetime, timezone
-            >>> import pandas as pd
             >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [
-            ...     datetime(2024, 1, 1, tzinfo=timezone.utc),
-            ...     datetime(2024, 1, 2, tzinfo=timezone.utc),
-            ... ]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            Let's define a dataframe-agnostic function:
-
-            >>> def agnostic_replace_time_zone(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.replace_time_zone("Asia/Kathmandu").to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_replace_time_zone`:
-
-            >>> agnostic_replace_time_zone(s_pd)
-            0   2024-01-01 00:00:00+05:45
-            1   2024-01-02 00:00:00+05:45
-            dtype: datetime64[ns, Asia/Kathmandu]
-
-            >>> agnostic_replace_time_zone(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            >>> s_native = pl.Series(
+            ...     [
+            ...         datetime(2024, 1, 1, tzinfo=timezone.utc),
+            ...         datetime(2024, 1, 2, tzinfo=timezone.utc),
+            ...     ]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.replace_time_zone(
+            ...     "Asia/Kathmandu"
+            ... ).to_native()  # doctest: +NORMALIZE_WHITESPACE
             shape: (2,)
             Series: '' [datetime[μs, Asia/Kathmandu]]
             [
-                2024-01-01 00:00:00 +0545
-                2024-01-02 00:00:00 +0545
-            ]
-
-            >>> agnostic_replace_time_zone(s_pa)
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                2023-12-31 18:15:00.000000Z,
-                2024-01-01 18:15:00.000000Z
-              ]
+                    2024-01-01 00:00:00 +0545
+                    2024-01-02 00:00:00 +0545
             ]
         """
         return self._narwhals_series._from_compliant_series(
@@ -1161,49 +602,18 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Examples:
             >>> from datetime import datetime, timezone
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [
-            ...     datetime(2024, 1, 1, tzinfo=timezone.utc),
-            ...     datetime(2024, 1, 2, tzinfo=timezone.utc),
-            ... ]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            Let's define a dataframe-agnostic function:
-
-            >>> def agnostic_convert_time_zone(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.convert_time_zone("Asia/Kathmandu").to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_convert_time_zone`:
-
-            >>> agnostic_convert_time_zone(s_pd)
+            >>> s_native = pd.Series(
+            ...     [
+            ...         datetime(2024, 1, 1, tzinfo=timezone.utc),
+            ...         datetime(2024, 1, 2, tzinfo=timezone.utc),
+            ...     ]
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.convert_time_zone("Asia/Kathmandu").to_native()
             0   2024-01-01 05:45:00+05:45
             1   2024-01-02 05:45:00+05:45
             dtype: datetime64[ns, Asia/Kathmandu]
-
-            >>> agnostic_convert_time_zone(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (2,)
-            Series: '' [datetime[μs, Asia/Kathmandu]]
-            [
-                2024-01-01 05:45:00 +0545
-                2024-01-02 05:45:00 +0545
-            ]
-
-            >>> agnostic_convert_time_zone(s_pa)
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                2024-01-01 00:00:00.000000Z,
-                2024-01-02 00:00:00.000000Z
-              ]
-            ]
         """
         if time_zone is None:
             msg = "Target `time_zone` cannot be `None` in `convert_time_zone`. Please use `replace_time_zone(None)` if you want to remove the time zone."
@@ -1225,49 +635,16 @@ class SeriesDateTimeNamespace(Generic[SeriesT]):
         Examples:
             >>> from datetime import date
             >>> import pandas as pd
-            >>> import polars as pl
-            >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [date(2001, 1, 1), None, date(2001, 1, 3)]
-            >>> s_pd = pd.Series(data, dtype="datetime64[ns]")
-            >>> s_pl = pl.Series(data)
-            >>> s_pa = pa.chunked_array([data])
-
-            Let's define a dataframe-agnostic function:
-
-            >>> def agnostic_timestamp(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.dt.timestamp("ms").to_native()
-
-            We can then pass any supported library such as pandas, Polars, or
-            PyArrow to `agnostic_timestamp`:
-
-            >>> agnostic_timestamp(s_pd)
+            >>> s_native = pd.Series(
+            ...     [date(2001, 1, 1), None, date(2001, 1, 3)], dtype="datetime64[ns]"
+            ... )
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.dt.timestamp("ms").to_native()
             0    9.783072e+11
             1             NaN
             2    9.784800e+11
             dtype: float64
-
-            >>> agnostic_timestamp(s_pl)  # doctest: +NORMALIZE_WHITESPACE
-            shape: (3,)
-            Series: '' [i64]
-            [
-                    978307200000
-                    null
-                    978480000000
-            ]
-
-            >>> agnostic_timestamp(s_pa)
-            <pyarrow.lib.ChunkedArray object at ...>
-            [
-              [
-                978307200000,
-                null,
-                978480000000
-              ]
-            ]
         """
         if time_unit not in {"ns", "us", "ms"}:
             msg = (
diff --git a/narwhals/series_list.py b/narwhals/series_list.py
index 19de071e..70f2133a 100644
--- a/narwhals/series_list.py
+++ b/narwhals/series_list.py
@@ -27,42 +27,11 @@ class SeriesListNamespace(Generic[SeriesT]):
             A new series.
 
         Examples:
-            >>> import pandas as pd
-            >>> import polars as pl
             >>> import pyarrow as pa
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoSeriesT
-
-            >>> data = [[1, 2], [3, 4, None], None, []]
-
-            Let's define a dataframe-agnostic function:
-
-            >>> def agnostic_list_len(s_native: IntoSeriesT) -> IntoSeriesT:
-            ...     s = nw.from_native(s_native, series_only=True)
-            ...     return s.list.len().to_native()
-
-            We can then pass pandas / PyArrow / Polars / any other supported library:
-
-            >>> agnostic_list_len(
-            ...     pd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64())))
-            ... )  # doctest: +SKIP
-            0       2
-            1       3
-            2    <NA>
-            3       0
-            dtype: int32[pyarrow]
-
-            >>> agnostic_list_len(pl.Series(data))  # doctest: +NORMALIZE_WHITESPACE
-            shape: (4,)
-            Series: '' [u32]
-            [
-               2
-               3
-               null
-               0
-            ]
-
-            >>> agnostic_list_len(pa.chunked_array([data]))  # doctest: +ELLIPSIS
+            >>> s_native = pa.chunked_array([[[1, 2], [3, 4, None], None, []]])
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.list.len().to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
               [
diff --git a/narwhals/series_str.py b/narwhals/series_str.py
index 98e42807..2a74ea36 100644
--- a/narwhals/series_str.py
+++ b/narwhals/series_str.py
@@ -186,22 +186,18 @@ class SeriesStringNamespace(Generic[SeriesT]):
             A new Series with boolean values indicating if each string contains the pattern.
 
         Examples:
-            >>> import pyarrow as pa
-            >>> import narwhals as nw
             >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> s_native = pa.chunked_array([["cat", "dog", "rabbit and parrot"]])
             >>> s = nw.from_native(s_native, series_only=True)
-            >>> s.str.contains(
-            ...     "cat|parrot"
-            ... ).to_native()  # doctest: +ELLIPSIS  +NORMALIZE_WHITESPACE
+            >>> s.str.contains("cat|parrot").to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
-            [
+              [
                 true,
                 false,
                 true
-            ]
+              ]
             ]
         """
         return self._narwhals_series._from_compliant_series(
@@ -281,14 +277,14 @@ class SeriesStringNamespace(Generic[SeriesT]):
             >>> import narwhals as nw
             >>> s_native = pa.chunked_array([["taata", "taatatata", "zukkyun"]])
             >>> s = nw.from_native(s_native, series_only=True)
-            >>> s.str.head().to_native()  # doctest: +ELLIPSIS  +NORMALIZE_WHITESPACE
+            >>> s.str.head().to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
-            [
+              [
                 "taata",
                 "taata",
                 "zukky"
-            ]
+              ]
             ]
         """
         return self._narwhals_series._from_compliant_series(
@@ -314,14 +310,14 @@ class SeriesStringNamespace(Generic[SeriesT]):
             >>> import narwhals as nw
             >>> s_native = pa.chunked_array([["taata", "taatatata", "zukkyun"]])
             >>> s = nw.from_native(s_native, series_only=True)
-            >>> s.str.tail().to_native()  # doctest: +ELLIPSIS  +NORMALIZE_WHITESPACE
+            >>> s.str.tail().to_native()  # doctest: +ELLIPSIS
             <pyarrow.lib.ChunkedArray object at ...>
             [
-            [
+              [
                 "taata",
                 "atata",
                 "kkyun"
-            ]
+              ]
             ]
         """
         return self._narwhals_series._from_compliant_series(
@@ -377,10 +373,11 @@ class SeriesStringNamespace(Generic[SeriesT]):
         """Parse Series with strings to a Series with Datetime dtype.
 
         Notes:
-            pandas defaults to nanosecond time unit, Polars to microsecond.
-            Prior to pandas 2.0, nanoseconds were the only time unit supported
-            in pandas, with no ability to set any other one. The ability to
-            set the time unit in pandas, if the version permits, will arrive.
+            - pandas defaults to nanosecond time unit, Polars to microsecond.
+              Prior to pandas 2.0, nanoseconds were the only time unit supported
+              in pandas, with no ability to set any other one. The ability to
+              set the time unit in pandas, if the version permits, will arrive.
+            - timezone-aware strings are all converted to and parsed as UTC.
 
         Warning:
             As different backends auto-infer format in different ways, if `format=None`
diff --git a/narwhals/typing.py b/narwhals/typing.py
index b908e671..d6bd8d3d 100644
--- a/narwhals/typing.py
+++ b/narwhals/typing.py
@@ -27,7 +27,9 @@ else:
 
 if TYPE_CHECKING:
     from types import ModuleType
+    from typing import Iterable
     from typing import Mapping
+    from typing import Sized
 
     import numpy as np
     from typing_extensions import Self
@@ -53,8 +55,8 @@ if TYPE_CHECKING:
 
         def join(self, *args: Any, **kwargs: Any) -> Any: ...
 
-    class NativeSeries(Protocol):
-        def __len__(self) -> int: ...
+    class NativeSeries(Sized, Iterable[Any], Protocol):
+        def filter(self, *args: Any, **kwargs: Any) -> Any: ...
 
     class DataFrameLike(Protocol):
         def __dataframe__(self, *args: Any, **kwargs: Any) -> Any: ...
@@ -65,8 +67,11 @@ class CompliantSeries(Protocol):
     def dtype(self) -> DType: ...
     @property
     def name(self) -> str: ...
+    @property
+    def native(self) -> Any: ...
     def __narwhals_series__(self) -> CompliantSeries: ...
     def alias(self, name: str) -> Self: ...
+    def _from_native_series(self, series: Any) -> Self: ...
 
 
 CompliantSeriesT_co = TypeVar(
@@ -188,7 +193,9 @@ class CompliantExpr(Protocol38[CompliantFrameT, CompliantSeriesT_co]):
         *,
         return_dtype: DType | type[DType] | None,
     ) -> Self: ...
-    def over(self: Self, keys: Sequence[str], kind: ExprKind) -> Self: ...
+    def over(
+        self: Self, keys: Sequence[str], kind: ExprKind, order_by: Sequence[str] | None
+    ) -> Self: ...
     def sample(
         self,
         n: int | None,
@@ -237,7 +244,7 @@ class CompliantExpr(Protocol38[CompliantFrameT, CompliantSeriesT_co]):
         self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self: ...
 
@@ -246,7 +253,7 @@ class CompliantExpr(Protocol38[CompliantFrameT, CompliantSeriesT_co]):
         self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
     ) -> Self: ...
 
@@ -255,7 +262,7 @@ class CompliantExpr(Protocol38[CompliantFrameT, CompliantSeriesT_co]):
         self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self: ...
@@ -265,7 +272,7 @@ class CompliantExpr(Protocol38[CompliantFrameT, CompliantSeriesT_co]):
         self,
         window_size: int,
         *,
-        min_samples: int | None,
+        min_samples: int,
         center: bool,
         ddof: int,
     ) -> Self: ...
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 43844171..0fa1f312 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -13,6 +13,7 @@ from typing import Callable
 from typing import Container
 from typing import Iterable
 from typing import Literal
+from typing import Protocol
 from typing import Sequence
 from typing import TypeVar
 from typing import Union
@@ -45,7 +46,6 @@ from narwhals.exceptions import InvalidOperationError
 if TYPE_CHECKING:
     from types import ModuleType
     from typing import AbstractSet as Set
-    from typing import Protocol
 
     import pandas as pd
     from typing_extensions import Self
@@ -61,7 +61,6 @@ if TYPE_CHECKING:
     from narwhals.typing import CompliantFrameT
     from narwhals.typing import CompliantLazyFrame
     from narwhals.typing import CompliantSeries
-    from narwhals.typing import CompliantSeriesT_co
     from narwhals.typing import DataFrameLike
     from narwhals.typing import DTypes
     from narwhals.typing import IntoSeriesT
@@ -118,6 +117,75 @@ if TYPE_CHECKING:
         def columns(self) -> Sequence[str]: ...
 
 
+NativeT_co = TypeVar("NativeT_co", covariant=True)
+CompliantT_co = TypeVar("CompliantT_co", covariant=True)
+CompliantExprT_co = TypeVar(
+    "CompliantExprT_co", bound="CompliantExpr[Any, Any]", covariant=True
+)
+CompliantSeriesT_co = TypeVar(
+    "CompliantSeriesT_co", bound="CompliantSeries", covariant=True
+)
+
+
+class _StoresNative(Protocol[NativeT_co]):
+    """Provides access to a native object.
+
+    Native objects have types like:
+
+    >>> from pandas import Series
+    >>> from pyarrow import Table
+    """
+
+    @property
+    def native(self) -> NativeT_co:
+        """Return the native object."""
+        ...
+
+
+class _StoresCompliant(Protocol[CompliantT_co]):
+    """Provides access to a compliant object.
+
+    Compliant objects have types like:
+
+    >>> from narwhals._pandas_like.series import PandasLikeSeries
+    >>> from narwhals._arrow.dataframe import ArrowDataFrame
+    """
+
+    @property
+    def compliant(self) -> CompliantT_co:
+        """Return the compliant object."""
+        ...
+
+
+class _SeriesNamespace(  # type: ignore[misc]  # noqa: PYI046
+    _StoresCompliant[CompliantSeriesT_co],
+    _StoresNative[NativeT_co],
+    Protocol[CompliantSeriesT_co, NativeT_co],
+):
+    _compliant_series: CompliantSeriesT_co
+
+    @property
+    def compliant(self) -> CompliantSeriesT_co:
+        return self._compliant_series
+
+    @property
+    def native(self) -> NativeT_co:
+        return self._compliant_series.native
+
+    def from_native(self, series: Any, /) -> CompliantSeriesT_co:
+        return self.compliant._from_native_series(series)
+
+
+class _ExprNamespace(  # type: ignore[misc] # noqa: PYI046
+    _StoresCompliant[CompliantExprT_co], Protocol[CompliantExprT_co]
+):
+    _compliant_expr: CompliantExprT_co
+
+    @property
+    def compliant(self) -> CompliantExprT_co:
+        return self._compliant_expr
+
+
 class Version(Enum):
     V1 = auto()
     MAIN = auto()
@@ -816,7 +884,7 @@ def maybe_set_index(
         msg = "Only one of `column_names` or `index` should be provided"
         raise ValueError(msg)
 
-    if not column_names and not index:
+    if not column_names and index is None:
         msg = "Either `column_names` or `index` should be provided"
         raise ValueError(msg)
 
diff --git a/pyproject.toml b/pyproject.toml
index 21a252f2..713e107f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -195,7 +195,7 @@ filterwarnings = [
   # This warning was temporarily raised by pandas but then reverted.
   'ignore:.*Passing a BlockManager to DataFrame:DeprecationWarning',
   # This warning was temporarily raised by Polars but then reverted.
-  'ignore:.*The default coalesce behavior of left join will change:DeprecationWarning:polars',
+  'ignore:.*The default coalesce behavior of left join will change:DeprecationWarning',
   'ignore: unclosed <socket.socket',
   'ignore:.*The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning:pyspark',
   'ignore:.*distutils Version classes are deprecated. Use packaging.version instead.*:DeprecationWarning:pyspark',
@@ -204,7 +204,9 @@ filterwarnings = [
   'ignore:.*Python binding for RankQuantileOptions not exposed:RuntimeWarning:pyarrow',
   'ignore:.*pandas only supports SQLAlchemy:UserWarning:sqlframe',
   'ignore:.*numpy.core is deprecated and has been renamed to numpy._core.*:DeprecationWarning:sqlframe',
-  "ignore:.*__array__ implementation doesn't accept a copy keyword, so passing copy=False failed:DeprecationWarning:modin"
+  "ignore:.*__array__ implementation doesn't accept a copy keyword, so passing copy=False failed:DeprecationWarning:modin",
+  # raised internally by pandas
+  "ignore:.*np.find_common_type is deprecated:DeprecationWarning:pandas",
 ]
 xfail_strict = true
 markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]


Test patch:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 6bb570e5..790e4565 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -28,7 +28,7 @@ jobs:
           cache-dependency-glob: "pyproject.toml"
       - name: install-reqs
         # Python3.8 is technically at end-of-life, so we don't test everything
-        run: uv pip install -e ".[tests, core]" --system
+        run: uv pip install -e ".[tests, core]" backports.zoneinfo --system
       - name: show-deps
         run: uv pip freeze
       - name: Run pytest
diff --git a/tests/conftest.py b/tests/conftest.py
index bf5d97b6..244d31b9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
     from narwhals.typing import IntoDataFrame
     from narwhals.typing import IntoFrame
 
-MIN_PANDAS_NULLABLE_VERSION = (1, 5)
+MIN_PANDAS_NULLABLE_VERSION = (2,)
 
 # When testing cudf.pandas in Kaggle, we get an error if we try to run
 # python -m cudf.pandas -m pytest --constructors=pandas. This gives us
diff --git a/tests/expr_and_series/cum_sum_test.py b/tests/expr_and_series/cum_sum_test.py
index eb1b985b..8ea8cb5e 100644
--- a/tests/expr_and_series/cum_sum_test.py
+++ b/tests/expr_and_series/cum_sum_test.py
@@ -3,6 +3,8 @@ from __future__ import annotations
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import POLARS_VERSION
+from tests.utils import Constructor
 from tests.utils import ConstructorEager
 from tests.utils import assert_equal_data
 
@@ -24,6 +26,190 @@ def test_cum_sum_expr(constructor_eager: ConstructorEager, *, reverse: bool) ->
     assert_equal_data(result, {name: expected[name]})
 
 
+@pytest.mark.parametrize(
+    ("reverse", "expected_a"),
+    [
+        (False, [3, 2, 6]),
+        (True, [4, 6, 3]),
+    ],
+)
+def test_lazy_cum_sum_grouped(
+    constructor: Constructor,
+    request: pytest.FixtureRequest,
+    *,
+    reverse: bool,
+    expected_a: list[int],
+) -> None:
+    if "duckdb" in str(constructor):
+        # no window function support yet in duckdb
+        request.applymarker(pytest.mark.xfail)
+    if "pyarrow_table" in str(constructor):
+        # grouped window functions not yet supported
+        request.applymarker(pytest.mark.xfail)
+    if "modin" in str(constructor):
+        # bugged
+        request.applymarker(pytest.mark.xfail)
+    if "dask" in str(constructor):
+        # https://github.com/dask/dask/issues/11806
+        request.applymarker(pytest.mark.xfail)
+    if "polars" in str(constructor) and POLARS_VERSION < (1, 9):
+        pytest.skip(reason="too old version")
+
+    df = nw.from_native(
+        constructor(
+            {
+                "a": [1, 2, 3],
+                "b": [1, 0, 2],
+                "i": [0, 1, 2],
+                "g": [1, 1, 1],
+            }
+        )
+    )
+    result = df.with_columns(
+        nw.col("a").cum_sum(reverse=reverse).over("g", _order_by="b")
+    ).sort("i")
+    expected = {"a": expected_a, "b": [1, 0, 2], "i": [0, 1, 2]}
+    assert_equal_data(result, expected)
+
+
+@pytest.mark.parametrize(
+    ("reverse", "expected_a"),
+    [
+        (False, [10, 6, 14, 11, 16, 9, 4]),
+        (True, [7, 12, 5, 6, 2, 10, 16]),
+    ],
+)
+def test_lazy_cum_sum_ordered_by_nulls(
+    constructor: Constructor,
+    request: pytest.FixtureRequest,
+    *,
+    reverse: bool,
+    expected_a: list[int],
+) -> None:
+    if "duckdb" in str(constructor):
+        # no window function support yet in duckdb
+        request.applymarker(pytest.mark.xfail)
+    if "pyarrow_table" in str(constructor):
+        # grouped window functions not yet supported
+        request.applymarker(pytest.mark.xfail)
+    if "modin" in str(constructor):
+        # bugged
+        request.applymarker(pytest.mark.xfail)
+    if "dask" in str(constructor):
+        # https://github.com/dask/dask/issues/11806
+        request.applymarker(pytest.mark.xfail)
+    if "polars" in str(constructor) and POLARS_VERSION < (1, 9):
+        pytest.skip(reason="too old version")
+
+    df = nw.from_native(
+        constructor(
+            {
+                "a": [1, 2, 3, 1, 2, 3, 4],
+                "b": [1, -1, 3, 2, 5, 0, None],
+                "i": [0, 1, 2, 3, 4, 5, 6],
+                "g": [1, 1, 1, 1, 1, 1, 1],
+            }
+        )
+    )
+    result = df.with_columns(
+        nw.col("a").cum_sum(reverse=reverse).over("g", _order_by="b")
+    ).sort("i")
+    expected = {
+        "a": expected_a,
+        "b": [1, -1, 3, 2, 5, 0, None],
+        "i": [0, 1, 2, 3, 4, 5, 6],
+    }
+    assert_equal_data(result, expected)
+
+
+@pytest.mark.parametrize(
+    ("reverse", "expected_a"),
+    [
+        (False, [3, 2, 6]),
+        (True, [4, 6, 3]),
+    ],
+)
+def test_lazy_cum_sum_ungrouped(
+    constructor: Constructor,
+    request: pytest.FixtureRequest,
+    *,
+    reverse: bool,
+    expected_a: list[int],
+) -> None:
+    if "duckdb" in str(constructor):
+        # no window function support yet in duckdb
+        request.applymarker(pytest.mark.xfail)
+    if "dask" in str(constructor) and reverse:
+        # https://github.com/dask/dask/issues/11802
+        request.applymarker(pytest.mark.xfail)
+    if "modin" in str(constructor):
+        # probably bugged
+        request.applymarker(pytest.mark.xfail)
+    if "polars" in str(constructor) and POLARS_VERSION < (1, 9):
+        pytest.skip(reason="too old version")
+
+    df = nw.from_native(
+        constructor(
+            {
+                "a": [2, 3, 1],
+                "b": [0, 2, 1],
+                "i": [1, 2, 0],
+            }
+        )
+    ).sort("i")
+    result = df.with_columns(
+        nw.col("a").cum_sum(reverse=reverse).over(_order_by="b")
+    ).sort("i")
+    expected = {"a": expected_a, "b": [1, 0, 2], "i": [0, 1, 2]}
+    assert_equal_data(result, expected)
+
+
+@pytest.mark.parametrize(
+    ("reverse", "expected_a"),
+    [
+        (False, [10, 6, 14, 11, 16, 9, 4]),
+        (True, [7, 12, 5, 6, 2, 10, 16]),
+    ],
+)
+def test_lazy_cum_sum_ungrouped_ordered_by_nulls(
+    constructor: Constructor,
+    request: pytest.FixtureRequest,
+    *,
+    reverse: bool,
+    expected_a: list[int],
+) -> None:
+    if "duckdb" in str(constructor):
+        # no window function support yet in duckdb
+        request.applymarker(pytest.mark.xfail)
+    if "dask" in str(constructor):
+        # https://github.com/dask/dask/issues/11806
+        request.applymarker(pytest.mark.xfail)
+    if "modin" in str(constructor):
+        # probably bugged
+        request.applymarker(pytest.mark.xfail)
+    if "polars" in str(constructor) and POLARS_VERSION < (1, 9):
+        pytest.skip(reason="too old version")
+
+    df = nw.from_native(
+        constructor(
+            {
+                "a": [1, 2, 3, 1, 2, 3, 4],
+                "b": [1, -1, 3, 2, 5, 0, None],
+                "i": [0, 1, 2, 3, 4, 5, 6],
+            }
+        )
+    ).sort("i")
+    result = df.with_columns(
+        nw.col("a").cum_sum(reverse=reverse).over(_order_by="b")
+    ).sort("i")
+    expected = {
+        "a": expected_a,
+        "b": [1, -1, 3, 2, 5, 0, None],
+        "i": [0, 1, 2, 3, 4, 5, 6],
+    }
+    assert_equal_data(result, expected)
+
+
 def test_cum_sum_series(constructor_eager: ConstructorEager) -> None:
     df = nw.from_native(constructor_eager(data), eager_only=True)
     result = df.select(
diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py
index 5c72dc00..38bfdd2c 100644
--- a/tests/expr_and_series/over_test.py
+++ b/tests/expr_and_series/over_test.py
@@ -10,6 +10,7 @@ import pytest
 import narwhals.stable.v1 as nw
 from narwhals.exceptions import LengthChangingExprError
 from tests.utils import PANDAS_VERSION
+from tests.utils import POLARS_VERSION
 from tests.utils import Constructor
 from tests.utils import ConstructorEager
 from tests.utils import assert_equal_data
@@ -247,7 +248,7 @@ def test_over_anonymous_cumulative(
     if "cudf" in str(constructor_eager):
         # https://github.com/rapidsai/cudf/issues/18159
         request.applymarker(pytest.mark.xfail)
-    df = nw.from_native(constructor_eager({"a": [1, 1, 2], "b": [4, 5, 6]}))
+    df = nw.from_native(constructor_eager({"": [1, 1, 2], "b": [4, 5, 6]}))
     context = (
         pytest.raises(NotImplementedError)
         if df.implementation.is_pyarrow()
@@ -260,12 +261,12 @@ def test_over_anonymous_cumulative(
     )
     with context:
         result = df.with_columns(
-            nw.all().cum_sum().over("a").name.suffix("_cum_sum")
-        ).sort("a", "b")
+            nw.all().cum_sum().over("").name.suffix("_cum_sum")
+        ).sort("", "b")
         expected = {
-            "a": [1, 1, 2],
+            "": [1, 1, 2],
             "b": [4, 5, 6],
-            "a_cum_sum": [1, 2, 2],
+            "_cum_sum": [1, 2, 2],
             "b_cum_sum": [4, 9, 6],
         }
         assert_equal_data(result, expected)
@@ -413,3 +414,24 @@ def test_unsupported_over() -> None:
     tbl = pa.table(data)  # type: ignore[arg-type]
     with pytest.raises(NotImplementedError, match="aggregation or literal"):
         nw.from_native(tbl).select(nw.col("a").shift(1).cum_sum().over("b"))
+
+
+def test_over_without_partition_by(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
+    if "polars" in str(constructor) and POLARS_VERSION < (1, 10):
+        pytest.skip()
+    if "duckdb" in str(constructor):
+        # windows not yet supported
+        request.applymarker(pytest.mark.xfail)
+    if "modin" in str(constructor):
+        # probably bugged
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(constructor({"a": [1, -1, 2], "i": [0, 2, 1]}))
+    result = (
+        df.with_columns(b=nw.col("a").abs().cum_sum().over(_order_by="i"))
+        .sort("i")
+        .select("a", "b", "i")
+    )
+    expected = {"a": [1, 2, -1], "b": [1, 3, 4], "i": [0, 1, 2]}
+    assert_equal_data(result, expected)
diff --git a/tests/expr_and_series/rolling_sum_test.py b/tests/expr_and_series/rolling_sum_test.py
index 2b9e273a..bcace792 100644
--- a/tests/expr_and_series/rolling_sum_test.py
+++ b/tests/expr_and_series/rolling_sum_test.py
@@ -12,6 +12,8 @@ from hypothesis import given
 import narwhals.stable.v1 as nw
 from narwhals.exceptions import InvalidOperationError
 from tests.utils import PANDAS_VERSION
+from tests.utils import POLARS_VERSION
+from tests.utils import Constructor
 from tests.utils import ConstructorEager
 from tests.utils import assert_equal_data
 
@@ -54,6 +56,109 @@ def test_rolling_sum_expr(constructor_eager: ConstructorEager) -> None:
     assert_equal_data(result, expected)
 
 
+@pytest.mark.filterwarnings(
+    "ignore:`Expr.rolling_sum` is being called from the stable API although considered an unstable feature."
+)
+@pytest.mark.parametrize(
+    ("expected_a", "window_size", "min_samples", "center"),
+    [
+        ([None, None, 3, None, None, 10, 17], 2, None, False),
+        ([None, None, 3, None, None, 10, 17], 2, 2, False),
+        ([None, None, 3, 3, 6, 10, 21], 3, 2, False),
+        ([1, None, 3, 3, 6, 10, 21], 3, 1, False),
+        ([3, 1, 3, 6, 10, 21, 17], 3, 1, True),
+        ([3, 1, 3, 7, 12, 21, 21], 4, 1, True),
+        ([3, 3, 7, 13, 23, 21, 21], 5, 1, True),
+    ],
+)
+def test_rolling_sum_expr_lazy_ungrouped(
+    constructor: Constructor,
+    expected_a: list[float],
+    window_size: int,
+    min_samples: int,
+    request: pytest.FixtureRequest,
+    *,
+    center: bool,
+) -> None:
+    if "polars" in str(constructor) and POLARS_VERSION < (1, 10):
+        pytest.skip()
+    if "duckdb" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+    if "modin" in str(constructor):
+        # unreliable
+        pytest.skip()
+    data = {
+        "a": [1, None, 2, None, 4, 6, 11],
+        "b": [1, None, 2, 3, 4, 5, 6],
+        "i": list(range(7)),
+    }
+    df = nw.from_native(constructor(data))
+    result = (
+        df.with_columns(
+            nw.col("a")
+            .rolling_sum(window_size, min_samples=min_samples, center=center)
+            .over(_order_by="b")
+        )
+        .select("a", "i")
+        .sort("i")
+    )
+    expected = {"a": expected_a, "i": list(range(7))}
+    assert_equal_data(result, expected)
+
+
+@pytest.mark.filterwarnings(
+    "ignore:`Expr.rolling_sum` is being called from the stable API although considered an unstable feature."
+)
+@pytest.mark.parametrize(
+    ("expected_a", "window_size", "min_samples", "center"),
+    [
+        ([None, None, 3, None, None, 10, 17], 2, None, False),
+        ([None, None, 3, None, None, 10, 17], 2, 2, False),
+        ([None, None, 3, 3, None, 10, 21], 3, 2, False),
+        ([1, None, 3, 3, 4, 10, 21], 3, 1, False),
+        ([3, 1, 3, 2, 10, 21, 17], 3, 1, True),
+        ([3, 1, 3, 3, 10, 21, 21], 4, 1, True),
+        ([3, 3, 3, 3, 21, 21, 21], 5, 1, True),
+    ],
+)
+def test_rolling_sum_expr_lazy_grouped(
+    constructor: Constructor,
+    expected_a: list[float],
+    window_size: int,
+    min_samples: int,
+    request: pytest.FixtureRequest,
+    *,
+    center: bool,
+) -> None:
+    if "polars" in str(constructor) and POLARS_VERSION < (1, 10):
+        pytest.skip()
+    if "pandas" in str(constructor) and PANDAS_VERSION < (1, 2):
+        pytest.skip()
+    if any(x in str(constructor) for x in ("dask", "pyarrow_table", "duckdb")):
+        request.applymarker(pytest.mark.xfail)
+    if "modin" in str(constructor):
+        # unreliable
+        pytest.skip()
+    data = {
+        "a": [1, None, 2, None, 4, 6, 11],
+        "g": [1, 1, 1, 1, 2, 2, 2],
+        "b": [1, None, 2, 3, 4, 5, 6],
+        "i": list(range(7)),
+    }
+    df = nw.from_native(constructor(data))
+    result = (
+        df.with_columns(
+            nw.col("a")
+            .rolling_sum(window_size, min_samples=min_samples, center=center)
+            .over("g", _order_by="b")
+        )
+        .sort("i")
+        .select("a")
+    )
+    expected = {"a": expected_a}
+    assert_equal_data(result, expected)
+
+
 @pytest.mark.filterwarnings(
     "ignore:`Series.rolling_sum` is being called from the stable API although considered an unstable feature."
 )
diff --git a/tests/expr_and_series/str/to_datetime_test.py b/tests/expr_and_series/str/to_datetime_test.py
index 24687af9..e5ac7850 100644
--- a/tests/expr_and_series/str/to_datetime_test.py
+++ b/tests/expr_and_series/str/to_datetime_test.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+from contextlib import nullcontext as does_not_raise
 from datetime import datetime
 from datetime import timezone
 from typing import TYPE_CHECKING
@@ -9,7 +10,10 @@ import pytest
 
 import narwhals.stable.v1 as nw
 from narwhals._arrow.utils import parse_datetime_format
+from tests.utils import PANDAS_VERSION
+from tests.utils import PYARROW_VERSION
 from tests.utils import assert_equal_data
+from tests.utils import is_pyarrow_windows_no_tzdata
 
 if TYPE_CHECKING:
     from tests.utils import Constructor
@@ -28,10 +32,16 @@ def test_to_datetime(constructor: Constructor) -> None:
         nw.from_native(constructor(data))
         .lazy()
         .select(b=nw.col("a").str.to_datetime(format="%Y-%m-%dT%H:%M:%S"))
-        .collect()
-        .item(row=0, column="b")
     )
-    assert str(result) == expected
+    result_schema = result.collect_schema()
+    assert isinstance(result_schema["b"], nw.Datetime)
+    if "sqlframe" in str(constructor):
+        # https://github.com/eakmanrq/sqlframe/issues/326
+        assert result_schema["b"].time_zone == "UTC"  # pyright: ignore[reportAttributeAccessIssue]
+    else:
+        assert result_schema["b"].time_zone is None  # pyright: ignore[reportAttributeAccessIssue]
+    result_item = result.collect().item(row=0, column="b")
+    assert str(result_item) == expected
 
 
 def test_to_datetime_series(constructor_eager: ConstructorEager) -> None:
@@ -190,3 +200,39 @@ def test_pyarrow_infer_datetime_raise_inconsistent_date_fmt(
 ) -> None:
     with pytest.raises(ValueError, match="Unable to infer datetime format. "):
         parse_datetime_format(pa.chunked_array([data]))
+
+
+@pytest.mark.parametrize("format", [None, "%Y-%m-%dT%H:%M:%S%z"])
+def test_to_datetime_tz_aware(
+    constructor: Constructor,
+    request: pytest.FixtureRequest,
+    format: str | None,  # noqa: A002
+) -> None:
+    if "pyarrow_table" in str(constructor) and PYARROW_VERSION < (13,):
+        # bugged
+        pytest.skip()
+    if "pandas" in str(constructor) and PANDAS_VERSION < (1,):
+        # "Cannot pass a tz argument when parsing strings with timezone information."
+        pytest.skip()
+    if is_pyarrow_windows_no_tzdata(constructor):
+        pytest.skip()
+    if "sqlframe" in str(constructor):
+        # https://github.com/eakmanrq/sqlframe/issues/325
+        request.applymarker(pytest.mark.xfail)
+    context = (
+        pytest.raises(NotImplementedError)
+        if any(x in str(constructor) for x in ("duckdb", "sqlframe")) and format is None
+        else does_not_raise()
+    )
+    df = nw.from_native(constructor({"a": ["2020-01-01T01:02:03+0100"]}))
+    with context:
+        result = df.with_columns(b=nw.col("a").str.to_datetime(format))
+        assert isinstance(result.collect_schema()["b"], nw.Datetime)
+        result_schema = result.lazy().collect().schema
+        assert result_schema["a"] == nw.String
+        assert isinstance(result_schema["b"], nw.Datetime)
+        expected = {
+            "a": ["2020-01-01T01:02:03+0100"],
+            "b": [datetime(2020, 1, 1, 0, 2, 3, tzinfo=timezone.utc)],
+        }
+        assert_equal_data(result, expected)
diff --git a/tests/series_only/scatter_test.py b/tests/series_only/scatter_test.py
index d7f0aa77..26bb8867 100644
--- a/tests/series_only/scatter_test.py
+++ b/tests/series_only/scatter_test.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import pandas as pd
 import pytest
 
 import narwhals as nw
@@ -27,6 +28,13 @@ def test_scatter(
     assert_equal_data(result, expected)
 
 
+def test_scatter_indices() -> None:
+    s = nw.from_native(pd.Series([2, 3, 6], index=[1, 0, 2]), series_only=True)
+    result = s.scatter([1, 0, 2], s)
+    expected = pd.Series([3, 2, 6], index=[1, 0, 2])
+    pd.testing.assert_series_equal(result.to_native(), expected)
+
+
 def test_scatter_unchanged(constructor_eager: ConstructorEager) -> None:
     df = nw.from_native(
         constructor_eager({"a": [1, 2, 3], "b": [142, 124, 132]}), eager_only=True
diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py
index 7b48c8a7..ab607f4a 100644
--- a/tests/translate/from_native_test.py
+++ b/tests/translate/from_native_test.py
@@ -292,16 +292,20 @@ def test_from_mock_interchange_protocol_non_strict() -> None:
     assert result is mockdf
 
 
-def test_from_native_altair_array_like() -> None:
+def test_from_native_strict_native_series() -> None:
     obj: list[int] = [1, 2, 3, 4]
     array_like = cast("Iterable[Any]", obj)
     not_array_like: Literal[1] = 1
+    np_array = pl.Series(obj).to_numpy()
 
     with pytest.raises(TypeError, match="got.+list"):
-        false_positive_native_series = nw.from_native(obj, series_only=True)  # noqa: F841
+        nw.from_native(obj, series_only=True)  # type: ignore[call-overload]
 
     with pytest.raises(TypeError, match="got.+list"):
-        true_negative_iterable = nw.from_native(array_like, series_only=True)  # type: ignore[call-overload] # noqa: F841
+        nw.from_native(array_like, series_only=True)  # type: ignore[call-overload]
 
     with pytest.raises(TypeError, match="got.+int"):
-        true_negative_not_native_series = nw.from_native(not_array_like, series_only=True)  # type: ignore[call-overload] # noqa: F841
+        nw.from_native(not_array_like, series_only=True)  # type: ignore[call-overload]
+
+    with pytest.raises(TypeError, match="got.+numpy.ndarray"):
+        nw.from_native(np_array, series_only=True)  # type: ignore[call-overload]
diff --git a/tests/utils.py b/tests/utils.py
index b05b93dd..cc068196 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -87,10 +87,6 @@ def _sort_dict_by_key(
 
 
 def assert_equal_data(result: Any, expected: Mapping[str, Any]) -> None:
-    is_pyspark = (
-        hasattr(result, "_compliant_frame")
-        and result.implementation is Implementation.PYSPARK
-    )
     is_duckdb = (
         hasattr(result, "_compliant_frame")
         and result._compliant_frame._implementation is Implementation.DUCKDB
@@ -111,10 +107,6 @@ def assert_equal_data(result: Any, expected: Mapping[str, Any]) -> None:
         for idx, (col, key) in enumerate(zip(result.columns, expected.keys())):
             assert col == key, f"Expected column name {key} at index {idx}, found {col}"
     result = {key: _to_comparable_list(result[key]) for key in expected}
-    if (is_pyspark or is_duckdb) and expected:  # pragma: no cover
-        sort_key = next(iter(expected.keys()))
-        expected = _sort_dict_by_key(expected, sort_key)
-        result = _sort_dict_by_key(result, sort_key)
     assert list(result.keys()) == list(expected.keys()), (
         f"Result keys {result.keys()}, expected keys: {expected.keys()}"
     )