Repository: narwhals-dev/narwhals
Pull request #1466 (2024-11-29 19:25:55)

docs: clean lazyframe docstrings
<!--
# 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

NA

## Checklist

- [x] Code follows style guide (ruff)
- [ ] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below
Cleans _lazyframe_ docstrings
- All references to lazyframes as `lf_pl`
- `from_native`/`to_native` instead of `narwhalify`
- `agnostic_func` instead of `func`


Patch:
diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml
index 17986a7d..74bd5a31 100644
--- a/.github/workflows/publish_to_pypi.yml
+++ b/.github/workflows/publish_to_pypi.yml
@@ -7,12 +7,8 @@ on:
 
 
 jobs:
-  slow-downstream-tests:
-    uses: ./.github/workflows/downstream_tests_slow.yml
-
   build:
     name: Build distribution 📦
-    needs: slow-downstream-tests
     runs-on: ubuntu-latest
 
     steps:
diff --git a/README.md b/README.md
index 6133a0ed..beaa789a 100644
--- a/README.md
+++ b/README.md
@@ -37,28 +37,21 @@ Get started!
 - [Join our community call](https://calendar.google.com/calendar/embed?src=27ff6dc5f598c1d94c1f6e627a1aaae680e2fac88f848bda1f2c7946ae74d5ab%40group.calendar.google.com)
 - [Read the contributing guide](https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md)
 
-## Used by
-
-Join the party!
-
-- [altair](https://github.com/vega/altair/)
-- [hierarchicalforecast](https://github.com/Nixtla/hierarchicalforecast)
-- [marimo](https://github.com/marimo-team/marimo)
-- [panel-graphic-walker](https://github.com/panel-extensions/panel-graphic-walker)
-- [plotly](https://plotly.com)
-- [pymarginaleffects](https://github.com/vincentarelbundock/pymarginaleffects)
-- [py-shiny](https://github.com/posit-dev/py-shiny)
-- [rio](https://github.com/rio-labs/rio)
-- [scikit-lego](https://github.com/koaning/scikit-lego)
-- [scikit-playtime](https://github.com/koaning/scikit-playtime)
-- [tabmat](https://github.com/Quantco/tabmat)
-- [timebasedcv](https://github.com/FBruzzesi/timebasedcv)
-- [tubular](https://github.com/lvgig/tubular)
-- [vegafusion](https://github.com/vega/vegafusion)
-- [wimsey](https://github.com/benrutter/wimsey)
-
-Feel free to add your project to the list if it's missing, and/or
-[chat with us on Discord](https://discord.gg/V3PqtB4VA4) if you'd like any support.
+<details>
+<summary>Table of contents</summary>
+
+- [Narwhals](#narwhals)
+  - [Installation](#installation)
+  - [Usage](#usage)
+  - [Example](#example)
+  - [Scope](#scope)
+  - [Roadmap](#roadmap)
+  - [Used by](#used-by)
+  - [Sponsors and institutional partners](#sponsors-and-institutional-partners)
+  - [Appears on](#appears-on)
+  - [Why "Narwhals"?](#why-narwhals)
+
+</details>
 
 ## Installation
 
@@ -106,6 +99,34 @@ See the [tutorial](https://narwhals-dev.github.io/narwhals/basics/dataframe/) fo
 
 If you said yes to both, we'd love to hear from you!
 
+## Roadmap
+
+See [roadmap discussion on GitHub](https://github.com/narwhals-dev/narwhals/discussions/1370)
+for an up-to-date plan of future work.
+
+## Used by
+
+Join the party!
+
+- [altair](https://github.com/vega/altair/)
+- [hierarchicalforecast](https://github.com/Nixtla/hierarchicalforecast)
+- [marimo](https://github.com/marimo-team/marimo)
+- [panel-graphic-walker](https://github.com/panel-extensions/panel-graphic-walker)
+- [plotly](https://plotly.com)
+- [pymarginaleffects](https://github.com/vincentarelbundock/pymarginaleffects)
+- [py-shiny](https://github.com/posit-dev/py-shiny)
+- [rio](https://github.com/rio-labs/rio)
+- [scikit-lego](https://github.com/koaning/scikit-lego)
+- [scikit-playtime](https://github.com/koaning/scikit-playtime)
+- [tabmat](https://github.com/Quantco/tabmat)
+- [timebasedcv](https://github.com/FBruzzesi/timebasedcv)
+- [tubular](https://github.com/lvgig/tubular)
+- [vegafusion](https://github.com/vega/vegafusion)
+- [wimsey](https://github.com/benrutter/wimsey)
+
+Feel free to add your project to the list if it's missing, and/or
+[chat with us on Discord](https://discord.gg/V3PqtB4VA4) if you'd like any support.
+
 ## Sponsors and institutional partners
 
 Narwhals is 100% independent, community-driven, and community-owned.
diff --git a/docs/backcompat.md b/docs/backcompat.md
index 787df393..8902d7a0 100644
--- a/docs/backcompat.md
+++ b/docs/backcompat.md
@@ -47,15 +47,26 @@ and deprecate the old one? The answer is...no!
 Narwhals offers a `stable` namespace, which allows you to write your code once and forget about
 it. That is to say, if you write your code like this:
 
-```python
-import narwhals.stable.v1 as nw
-from narwhals.typing import FrameT
+=== "from/to_native"
+    ```python
+    import narwhals.stable.v1 as nw
+    from narwhals.typing import IntoFrameT
 
 
-@nw.narwhalify
-def func(df: FrameT) -> FrameT:
-    return df.with_columns(nw.col("a").cum_sum())
-```
+    def func(df: IntoFrameT) -> IntoFrameT:
+        return nw.from_native(df).with_columns(nw.col("a").cum_sum()).to_native()
+    ```
+
+=== "@narwhalify"
+    ```python
+    import narwhals.stable.v1 as nw
+    from narwhals.typing import FrameT
+
+
+    @nw.narwhalify
+    def func(df: FrameT) -> FrameT:
+        return df.with_columns(nw.col("a").cum_sum())
+    ```
 
 then we, in Narwhals, promise that your code will keep working, even in newer versions of Polars
 after they have renamed their method.
diff --git a/docs/basics/complete_example.md b/docs/basics/complete_example.md
index d3b4ecfe..061d040f 100644
--- a/docs/basics/complete_example.md
+++ b/docs/basics/complete_example.md
@@ -22,31 +22,77 @@ doesn't either.
 We can specify that in the `@nw.narwhalify` decorator by setting `eager_only=True`, and
 the argument will be propagated to `nw.from_native`.
 
-```python
-import narwhals as nw
-from typing import Any
+=== "from/to_native"
+    ```python
+    from typing import Self
+    import narwhals as nw
+    from narwhals.typing import IntoDataFrameT
+
+
+    class StandardScaler:
+        def fit(self: Self, df: IntoDataFrameT) -> Self:
+            df_nw = nw.from_native(df, eager_only=True)
+            self._means = {col: df_nw[col].mean() for col in df_nw.columns}
+            self._std_devs = {col: df_nw[col].std() for col in df_nw.columns}
+            self._columns = df_nw.columns
+            return self
+    ```
+
+=== "@narwhalify"
+    ```python
+    from typing import Self
+    import narwhals as nw
+    from narwhals.typing import DataFrameT
 
 
-class StandardScaler:
-    @nw.narwhalify(eager_only=True)
-    def fit(self, df: nw.DataFrame[Any]) -> None:
-        self._means = {col: df[col].mean() for col in df.columns}
-        self._std_devs = {col: df[col].std() for col in df.columns}
-        self._columns = df.columns
-```
+    class StandardScaler:
+        @nw.narwhalify(eager_only=True)
+        def fit(self: Self, df: DataFrameT) -> Self:
+            self._means = {col: df[col].mean() for col in df.columns}
+            self._std_devs = {col: df[col].std() for col in df.columns}
+            self._columns = df.columns
+            return self
+    ```
 
 ## Transform method
 
-We're going to take in a dataframe, and return a dataframe of the same type.
-Therefore, we use `@nw.narwhalify`:
+We're going to take in a dataframe, and return a dataframe of the same type:
+
+=== "from/to_native"
+    ```python
+    from typing import Self
+    import narwhals as nw
+    from narwhals.typing import IntoFrameT
+
+
+    class StandardScaler:
+        ...
+
+        def transform(self: Self, df: IntoFrameT) -> IntoFrameT:
+            df_nw = nw.from_native(df)
+            return df_nw.with_columns(
+                (nw.col(col) - self._means[col]) / self._std_devs[col]
+                for col in self._columns
+            ).to_native()
+    ```
+
+=== "@narwhalify"
+    ```python
+    from typing import Self
+    import narwhals as nw
+    from narwhals.typing import FrameT
+
 
-```python
-@nw.narwhalify
-def transform(self, df: FrameT) -> FrameT:
-    return df.with_columns(
-        (nw.col(col) - self._means[col]) / self._std_devs[col] for col in self._columns
-    )
-```
+    class StandardScaler:
+        ...
+
+        @nw.narwhalify
+        def transform(self: Self, df: FrameT) -> FrameT:
+            return df.with_columns(
+                (nw.col(col) - self._means[col]) / self._std_devs[col]
+                for col in self._columns
+            )
+    ```
 
 Note that all the calculations here can stay lazy if the underlying library permits it,
 so we don't pass in any extra keyword-arguments such as `eager_only`, we just use the
@@ -55,34 +101,61 @@ default `eager_only=False`.
 ## Putting it all together
 
 Here is our dataframe-agnostic standard scaler:
-```python exec="1" source="above" session="tute-ex1"
-from typing import Any
 
-import narwhals as nw
-from narwhals.typing import FrameT
-
-
-class StandardScaler:
-    @nw.narwhalify(eager_only=True)
-    def fit(self, df: nw.DataFrame[Any]) -> None:
-        self._means = {col: df[col].mean() for col in df.columns}
-        self._std_devs = {col: df[col].std() for col in df.columns}
-        self._columns = df.columns
+=== "from/to_native"
+    ```python
+    from typing import Self
+    import narwhals as nw
+    from narwhals.typing import IntoDataFrameT
+    from narwhals.typing import IntoFrameT
+
+
+    class StandardScaler:
+        def fit(self: Self, df: IntoDataFrameT) -> Self:
+            df_nw = nw.from_native(df, eager_only=True)
+            self._means = {col: df_nw[col].mean() for col in df_nw.columns}
+            self._std_devs = {col: df_nw[col].std() for col in df_nw.columns}
+            self._columns = df_nw.columns
+            return self
+
+        def transform(self: Self, df: IntoFrameT) -> IntoFrameT:
+            df_nw = nw.from_native(df)
+            return df_nw.with_columns(
+                (nw.col(col) - self._means[col]) / self._std_devs[col]
+                for col in self._columns
+            ).to_native()
+    ```
 
-    @nw.narwhalify
-    def transform(self, df: FrameT) -> FrameT:
-        return df.with_columns(
-            (nw.col(col) - self._means[col]) / self._std_devs[col]
-            for col in self._columns
-        )
-```
+=== "@narwhalify"
+    ```python exec="1" source="above" session="standard-scaler-example"
+    from typing import Self
+    import narwhals as nw
+    from narwhals.typing import DataFrameT
+    from narwhals.typing import FrameT
+
+
+    class StandardScaler:
+        @nw.narwhalify(eager_only=True)
+        def fit(self: Self, df: DataFrameT) -> Self:
+            self._means = {col: df[col].mean() for col in df.columns}
+            self._std_devs = {col: df[col].std() for col in df.columns}
+            self._columns = df.columns
+            return self
+
+        @nw.narwhalify
+        def transform(self: Self, df: FrameT) -> FrameT:
+            return df.with_columns(
+                (nw.col(col) - self._means[col]) / self._std_devs[col]
+                for col in self._columns
+            )
+    ```
 
 Next, let's try running it. Notice how, as `transform` doesn't use
 any eager-only features, so we can pass a Polars LazyFrame to it and have it
 stay lazy!
 
 === "pandas"
-    ```python exec="true" source="material-block" result="python" session="tute-ex1"
+    ```python exec="true" source="material-block" result="python" session="standard-scaler-example"
     import pandas as pd
 
     df_train = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 7]})
@@ -93,7 +166,7 @@ stay lazy!
     ```
 
 === "Polars"
-    ```python exec="true" source="material-block" result="python" session="tute-ex1"
+    ```python exec="true" source="material-block" result="python" session="standard-scaler-example"
     import polars as pl
 
     df_train = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 7]})
diff --git a/docs/basics/dataframe.md b/docs/basics/dataframe.md
index 41be8aeb..78b9bcce 100644
--- a/docs/basics/dataframe.md
+++ b/docs/basics/dataframe.md
@@ -17,24 +17,42 @@ Let's explore this with some simple examples.
 
 ## Example 1: descriptive statistics
 
-Just like in Polars, we can pass expressions to
-`DataFrame.select` or `LazyFrame.select`.
+Just like in Polars, we can pass expressions to `DataFrame.select` or `LazyFrame.select`.
 
 Make a Python file with the following content:
 
-```python exec="1" source="above" session="df_ex1"
-import narwhals as nw
-from narwhals.typing import FrameT
+=== "from/to_native"
+    ```python exec="1" source="above" session="df_ex1"
+    import narwhals as nw
+    from narwhals.typing import IntoFrameT
+
+
+    def func(df: IntoFrameT) -> IntoFrameT:
+        return (
+            nw.from_native(df)
+            .select(
+                a_sum=nw.col("a").sum(),
+                a_mean=nw.col("a").mean(),
+                a_std=nw.col("a").std(),
+            )
+            .to_native()
+        )
+    ```
 
+=== "@narwhalify"
+    ```python exec="1" source="above" session="df_ex1"
+    import narwhals as nw
+    from narwhals.typing import FrameT
 
-@nw.narwhalify
-def func(df: FrameT) -> FrameT:
-    return df.select(
-        a_sum=nw.col("a").sum(),
-        a_mean=nw.col("a").mean(),
-        a_std=nw.col("a").std(),
-    )
-```
+
+    @nw.narwhalify
+    def func(df: FrameT) -> FrameT:
+        return df.select(
+            a_sum=nw.col("a").sum(),
+            a_mean=nw.col("a").mean(),
+            a_std=nw.col("a").std(),
+        )
+    ```
 
 Let's try it out:
 
@@ -70,42 +88,33 @@ Let's try it out:
     print(func(table))
     ```
 
-Alternatively, we could have opted for the more explicit version:
-
-```python
-import narwhals as nw
-from narwhals.typing import IntoFrameT
-
-
-def func(df_native: IntoFrameT) -> IntoFrameT:
-    df = nw.from_native(df_native)
-    df = df.select(
-        a_sum=nw.col("a").sum(),
-        a_mean=nw.col("a").mean(),
-        a_std=nw.col("a").std(),
-    )
-    return nw.to_native(df)
-```
-
-Despite being more verbose, it has the advantage of preserving the type annotation of the native
-object - see [typing](../api-reference/typing.md) for more details.
-
-In general, in this tutorial, we'll use the former.
-
 ## Example 2: group-by and mean
 
 Just like in Polars, we can pass expressions to `GroupBy.agg`.
 Make a Python file with the following content:
 
-```python exec="1" source="above" session="df_ex2"
-import narwhals as nw
-from narwhals.typing import FrameT
+=== "from/to_native"
+    ```python exec="1" source="above" session="df_ex2"
+    import narwhals as nw
+    from narwhals.typing import IntoFrameT
 
 
-@nw.narwhalify
-def func(df: FrameT) -> FrameT:
-    return df.group_by("a").agg(nw.col("b").mean()).sort("a")
-```
+    def func(df: IntoFrameT) -> IntoFrameT:
+        return (
+            nw.from_native(df).group_by("a").agg(nw.col("b").mean()).sort("a").to_native()
+        )
+    ```
+
+=== "@narwhalify"
+    ```python exec="1" source="above" session="df_ex2"
+    import narwhals as nw
+    from narwhals.typing import FrameT
+
+
+    @nw.narwhalify
+    def func(df: FrameT) -> FrameT:
+        return df.group_by("a").agg(nw.col("b").mean()).sort("a")
+    ```
 
 Let's try it out:
 
@@ -148,15 +157,30 @@ For example, we can compute a horizontal sum using `nw.sum_horizontal`.
 
 Make a Python file with the following content:
 
-```python exec="1" source="above" session="df_ex3"
-import narwhals as nw
-from narwhals.typing import FrameT
+=== "from/to_native"
+    ```python exec="1" source="above" session="df_ex3"
+    import narwhals as nw
+    from narwhals.typing import IntoFrameT
 
 
-@nw.narwhalify
-def func(df: FrameT) -> FrameT:
-    return df.with_columns(a_plus_b=nw.sum_horizontal("a", "b"))
-```
+    def func(df: IntoFrameT) -> IntoFrameT:
+        return (
+            nw.from_native(df)
+            .with_columns(a_plus_b=nw.sum_horizontal("a", "b"))
+            .to_native()
+        )
+    ```
+
+=== "@narwhalify"
+    ```python exec="1" source="above" session="df_ex3"
+    import narwhals as nw
+    from narwhals.typing import FrameT
+
+
+    @nw.narwhalify
+    def func(df: FrameT) -> FrameT:
+        return df.with_columns(a_plus_b=nw.sum_horizontal("a", "b"))
+    ```
 
 Let's try it out:
 
@@ -203,13 +227,12 @@ on a series.
 Make a Python file with the following content:
 
 ```python exec="1" source="above" session="df_ex4"
-from typing import Any
-
 import narwhals as nw
+from narwhals.typing import DataFrameT
 
 
 @nw.narwhalify(eager_only=True)
-def func(df: nw.DataFrame[Any], s: nw.Series, col_name: str) -> int:
+def func(df: DataFrameT, s: nw.Series, col_name: str) -> int:
     return df.filter(nw.col(col_name).is_in(s)).shape[0]
 ```
 
diff --git a/docs/basics/series.md b/docs/basics/series.md
index f5d8dee1..4cfc0292 100644
--- a/docs/basics/series.md
+++ b/docs/basics/series.md
@@ -11,20 +11,33 @@ to pass `eager_only=True` to `nw.from_native`.
 
 ## Example 1: filter based on a column's values
 
-This can stay lazy, so we just use `nw.from_native` and expressions:
+This can stay lazy, so we just use expressions:
 
-```python exec="1" source="above" session="ex1"
-import narwhals as nw
-from narwhals.typing import FrameT
+=== "from/to_native"
+    ```python exec="1" source="above" session="series_ex1"
+    import narwhals as nw
+    from narwhals.typing import IntoFrameT
 
 
-@nw.narwhalify
-def my_func(df: FrameT) -> FrameT:
-    return df.filter(nw.col("a") > 0)
-```
+    def my_func(df: IntoFrameT) -> IntoFrameT:
+        return nw.from_native(df).filter(nw.col("a") > 0).to_native()
+    ```
+
+=== "@narwhalify"
+    ```python exec="1" source="above" session="series_ex1"
+    import narwhals as nw
+    from narwhals.typing import FrameT
+
+
+    @nw.narwhalify
+    def my_func(df: FrameT) -> FrameT:
+        return df.filter(nw.col("a") > 0)
+    ```
+
+and call it either on a eager or lazy dataframe:
 
 === "pandas"
-    ```python exec="true" source="material-block" result="python" session="ex1"
+    ```python exec="true" source="material-block" result="python" session="series_ex1"
     import pandas as pd
 
     df = pd.DataFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -32,7 +45,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "Polars (eager)"
-    ```python exec="true" source="material-block" result="python" session="ex1"
+    ```python exec="true" source="material-block" result="python" session="series_ex1"
     import polars as pl
 
     df = pl.DataFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -40,7 +53,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "Polars (lazy)"
-    ```python exec="true" source="material-block" result="python" session="ex1"
+    ```python exec="true" source="material-block" result="python" session="series_ex1"
     import polars as pl
 
     df = pl.LazyFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -48,7 +61,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "PyArrow"
-    ```python exec="true" source="material-block" result="python" session="ex1"
+    ```python exec="true" source="material-block" result="python" session="series_ex1"
     import pyarrow as pa
 
     table = pa.table({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -60,18 +73,31 @@ def my_func(df: FrameT) -> FrameT:
 Let's write a dataframe-agnostic function which multiplies the values in column
 `'a'` by 2. This can also stay lazy, and can use expressions:
 
-```python exec="1" source="above" session="ex2"
-import narwhals as nw
-from narwhals.typing import FrameT
+=== "from/to_native"
+    ```python exec="1" source="above" session="series_ex2"
+    import narwhals as nw
+    from narwhals.typing import IntoFrameT
 
 
-@nw.narwhalify
-def my_func(df: FrameT) -> FrameT:
-    return df.with_columns(nw.col("a") * 2)
-```
+    def my_func(df: IntoFrameT) -> IntoFrameT:
+        return nw.from_native(df).with_columns(nw.col("a") * 2).to_native()
+    ```
+
+=== "@narwhalify"
+    ```python exec="1" source="above" session="series_ex2"
+    import narwhals as nw
+    from narwhals.typing import FrameT
+
+
+    @nw.narwhalify
+    def my_func(df: FrameT) -> FrameT:
+        return df.with_columns(nw.col("a") * 2)
+    ```
+
+and call it either on a eager or lazy dataframe:
 
 === "pandas"
-    ```python exec="true" source="material-block" result="python" session="ex2"
+    ```python exec="true" source="material-block" result="python" session="series_ex2"
     import pandas as pd
 
     df = pd.DataFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -79,7 +105,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "Polars (eager)"
-    ```python exec="true" source="material-block" result="python" session="ex2"
+    ```python exec="true" source="material-block" result="python" session="series_ex2"
     import polars as pl
 
     df = pl.DataFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -87,7 +113,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "Polars (lazy)"
-    ```python exec="true" source="material-block" result="python" session="ex2"
+    ```python exec="true" source="material-block" result="python" session="series_ex2"
     import polars as pl
 
     df = pl.LazyFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -95,7 +121,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "PyArrow"
-    ```python exec="true" source="material-block" result="python" session="ex2"
+    ```python exec="true" source="material-block" result="python" session="series_ex2"
     import pyarrow as pa
 
     table = pa.table({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -105,7 +131,7 @@ def my_func(df: FrameT) -> FrameT:
 Note that column `'a'` was overwritten. If we had wanted to add a new column called `'c'` containing column `'a'`'s
 values multiplied by 2, we could have used `Expr.alias`:
 
-```python exec="1" source="above" session="ex2.1"
+```python exec="1" source="above" session="series_ex2.1"
 import narwhals as nw
 from narwhals.typing import FrameT
 
@@ -116,7 +142,7 @@ def my_func(df: FrameT) -> FrameT:
 ```
 
 === "pandas"
-    ```python exec="true" source="material-block" result="python" session="ex2.1"
+    ```python exec="true" source="material-block" result="python" session="series_ex2.1"
     import pandas as pd
 
     df = pd.DataFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -124,7 +150,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "Polars (eager)"
-    ```python exec="true" source="material-block" result="python" session="ex2.1"
+    ```python exec="true" source="material-block" result="python" session="series_ex2.1"
     import polars as pl
 
     df = pl.DataFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -132,7 +158,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "Polars (lazy)"
-    ```python exec="true" source="material-block" result="python" session="ex2.1"
+    ```python exec="true" source="material-block" result="python" session="series_ex2.1"
     import polars as pl
 
     df = pl.LazyFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -140,7 +166,7 @@ def my_func(df: FrameT) -> FrameT:
     ```
 
 === "PyArrow"
-    ```python exec="true" source="material-block" result="python" session="ex2.1"
+    ```python exec="true" source="material-block" result="python" session="series_ex2.1"
     import pyarrow as pa
 
     table = pa.table({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -151,21 +177,34 @@ def my_func(df: FrameT) -> FrameT:
 
 Now, we want to find the mean of column `'a'`, and we need it as a Python scalar.
 This means that computation cannot stay lazy - it must execute!
-Therefore, we'll pass `eager_only=True` to `nw.narwhalify`, and then, instead
-of using expressions, we'll extract a `Series`.
+Therefore, we'll pass `eager_only=True` to `nw.from_native` (or `nw.narwhalify`),
+and then, instead of using expressions, we'll extract a `Series`.
 
-```python exec="1" source="above" session="ex2"
-from __future__ import annotations
-import narwhals as nw
+=== "from/to_native"
+    ```python exec="1" source="above" session="series_ex3"
+    import narwhals as nw
+    from narwhals.typing import IntoDataFrameT
 
 
-@nw.narwhalify(eager_only=True)
-def my_func(df: nw.DataFrame) -> float | None:
-    return df["a"].mean()
-```
+    def my_func(df: IntoDataFrameT) -> float | None:
+        return nw.from_native(df, eager_only=True)["a"].mean()
+    ```
+
+=== "@narwhalify"
+    ```python exec="1" source="above" session="series_ex3"
+    import narwhals as nw
+    from narwhals.typing import DataFrameT
+
+
+    @nw.narwhalify(eager_only=True)
+    def my_func(df: DataFrameT) -> float | None:
+        return df["a"].mean()
+    ```
+
+Now we can call it on a eager dataframe only:
 
 === "pandas"
-    ```python exec="true" source="material-block" result="python" session="ex2"
+    ```python exec="true" source="material-block" result="python" session="series_ex3"
     import pandas as pd
 
     df = pd.DataFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -173,7 +212,7 @@ def my_func(df: nw.DataFrame) -> float | None:
     ```
 
 === "Polars (eager)"
-    ```python exec="true" source="material-block" result="python" session="ex2"
+    ```python exec="true" source="material-block" result="python" session="series_ex3"
     import polars as pl
 
     df = pl.DataFrame({"a": [-1, 1, 3], "b": [3, 5, -3]})
@@ -181,7 +220,7 @@ def my_func(df: nw.DataFrame) -> float | None:
     ```
 
 === "PyArrow"
-    ```python exec="true" source="material-block" result="python" session="ex2"
+    ```python exec="true" source="material-block" result="python" session="series_ex3"
     import pyarrow as pa
 
     table = pa.table({"a": [-1, 1, 3], "b": [3, 5, -3]})
diff --git a/docs/css/code_select.css b/docs/css/code_select.css
deleted file mode 100644
index 259cf11c..00000000
--- a/docs/css/code_select.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.highlight .gp, .highlight .go { /* Generic.Prompt, Generic.Output */
-    user-select: none;
-    -webkit-user-select: none; /* Safari */
-    -moz-user-select: none; /* Firefox */
-    -ms-user-select: none; /* Internet Explorer/Edge */
-    color: red;
-}
\ No newline at end of file
diff --git a/docs/css/extra.css b/docs/css/extra.css
new file mode 100644
index 00000000..2d2769e0
--- /dev/null
+++ b/docs/css/extra.css
@@ -0,0 +1,4 @@
+.md-typeset ol li,
+.md-typeset ul li {
+  margin-bottom: 0.1em !important;
+}
\ No newline at end of file
diff --git a/docs/roadmap_and_related.md b/docs/ecosystem.md
similarity index 50%
rename from docs/roadmap_and_related.md
rename to docs/ecosystem.md
index e3f59c44..75e0f392 100644
--- a/docs/roadmap_and_related.md
+++ b/docs/ecosystem.md
@@ -1,9 +1,29 @@
-# Roadmap and related projects
+# Ecosystem
 
-## Roadmap
+## Used by
 
-See [roadmap discussion on GitHub](https://github.com/narwhals-dev/narwhals/discussions/1370)
-for an up-to-date plan of future work.
+The following is a non-exhaustive list of libraries and tools that choose to use Narwhals
+for their dataframe interoperability needs:
+
+* [altair](https://github.com/vega/altair/)
+* [hierarchicalforecast](https://github.com/Nixtla/hierarchicalforecast)
+* [marimo](https://github.com/marimo-team/marimo)
+* [panel-graphic-walker](https://github.com/panel-extensions/panel-graphic-walker)
+* [plotly](https://github.com/plotly/plotly.py)
+* [pymarginaleffects](https://github.com/vincentarelbundock/pymarginaleffects)
+* [py-shiny](https://github.com/posit-dev/py-shiny)
+* [rio](https://github.com/rio-labs/rio)
+* [scikit-lego](https://github.com/koaning/scikit-lego)
+* [scikit-playtime](https://github.com/koaning/scikit-playtime)
+* [tabmat](https://github.com/Quantco/tabmat)
+* [timebasedcv](https://github.com/FBruzzesi/timebasedcv)
+* [tubular](https://github.com/lvgig/tubular)
+* [vegafusion](https://github.com/vega/vegafusion)
+* [wimsey](https://github.com/benrutter/wimsey)
+
+If your project is missing from the list, feel free to open a PR to add it.
+
+If you would like to chat with us, or if you need any support, please [join our Discord server](https://discord.gg/V3PqtB4VA4).
 
 ## Related projects
 
@@ -21,7 +41,10 @@ Array counterpart to the DataFrame API, see [here](https://data-apis.org/array-a
 
 ### PyCapsule Interface
 
-Allows C extension modules to safely share pointers to C data structures with Python code and other C modules, encapsulating the pointer with a name and optional destructor to manage resources and ensure safe access, see [here](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html) for details. 
+Allows C extension modules to safely share pointers to C data structures with Python code
+and other C modules, encapsulating the pointer with a name and optional destructor to
+manage resources and ensure safe access,
+see [here](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html) for details.
 
 Narwhals supports exporting a DataFrame via the Arrow PyCapsule Interface.
 
@@ -30,30 +53,29 @@ Narwhals supports exporting a DataFrame via the Arrow PyCapsule Interface.
 Pitched as "The portable Dataframe library", Ibis provides a Pythonic frontend
 to various SQL (as well as Polars LazyFrame) engines. Some differences with Narwhals are:
 
-- Narwhals' main use case is for library maintainers wanting to support
+* Narwhals' main use case is for library maintainers wanting to support
   different dataframe libraries without depending on any whilst keeping
   things as lightweight as possible. Ibis is more targeted at end users
   and aims to be thought of as a Dataframe library akin to
   pandas / Polars / etc.
-- Narwhals allows you to write a "Dataframe X in, Dataframe X out" function.
+* Narwhals allows you to write a "Dataframe X in, Dataframe X out" function.
   Ibis allows materialising to pandas, Polars (eager), and PyArrow, but has
   no way to get back to the input type exactly (e.g. there's no way to
   start with a Polars LazyFrame and get back a Polars LazyFrame)
-- Narwhals respects input data types as much as possible, Ibis doesn't
+* Narwhals respects input data types as much as possible, Ibis doesn't
   support Categorical (nor does it distinguish between fixed-size-list and
   list)
-- Narwhals separates between lazy and eager APIs, with the eager API
+* Narwhals separates between lazy and eager APIs, with the eager API
   provide very fine control over dataframe operations (slicing rows and
   columns, iterating over rows, getting values out of the dataframe as
   Python scalars). Ibis is more focused on lazy execution
-- Ibis supports SQL engines (and can translate to SQL),
+* Ibis supports SQL engines (and can translate to SQL),
   Narwhals is more focused traditional dataframes where row-order is defined
   (although we are brainstorming a lazy-only level of support)
-- Narwhals is extremely lightweight and comes with zero required dependencies,
+* Narwhals is extremely lightweight and comes with zero required dependencies,
   Ibis requires pandas and PyArrow for all backends
-- Narwhals supports Dask, whereas Ibis has deprecated support for it
+* Narwhals supports Dask, whereas Ibis has deprecated support for it
 
 Although people often ask about the two tools, we consider them to be
 very different and not in competition. Further efforts to clarify the
 distinction are welcome 🙏!
-
diff --git a/docs/extending.md b/docs/extending.md
index 865a93b0..82729856 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -1,4 +1,4 @@
-# Extending Narwhals, levels of support
+# Supported libraries and extending Narwhals
 
 ## List of supported libraries (and how to add yours!)
 
@@ -15,7 +15,7 @@ Currently, Narwhals has full API support for the following libraries:
 It also has lazy-only support for [Dask](https://github.com/dask/dask), and interchange-only support
 for [DuckDB](https://github.com/duckdb/duckdb) and [Ibis](https://github.com/ibis-project/ibis).
 
-### Levels
+### Levels of support
 
 Narwhals comes with two levels of support ("full" and "interchange"), and we are working on defining
 a "lazy-only" level too.
@@ -25,18 +25,37 @@ Libraries for which we have full support can benefit from the whole
 
 For example:
 
-```python exec="1" source="above"
-import narwhals as nw
-from narwhals.typing import FrameT
-
-
-@nw.narwhalify
-def func(df: FrameT) -> FrameT:
-    return df.group_by("a").agg(
-        b_mean=nw.col("b").mean(),
-        b_std=nw.col("b").std(),
-    )
-```
+=== "from/to_native"
+    ```python exec="1" source="above"
+    import narwhals as nw
+    from narwhals.typing import IntoFrameT
+
+
+    def func(df: IntoFrameT) -> IntoFrameT:
+        return (
+            nw.from_native(df)
+            .group_by("a")
+            .agg(
+                b_mean=nw.col("b").mean(),
+                b_std=nw.col("b").std(),
+            )
+            .to_native()
+        )
+    ```
+
+=== "@narwhalify"
+    ```python exec="1" source="above"
+    import narwhals as nw
+    from narwhals.typing import FrameT
+
+
+    @nw.narwhalify
+    def func(df: FrameT) -> FrameT:
+        return df.group_by("a").agg(
+            b_mean=nw.col("b").mean(),
+            b_std=nw.col("b").std(),
+        )
+    ```
 
 will work for any of pandas, Polars, cuDF, Modin, and PyArrow.
 
@@ -74,7 +93,7 @@ converting to Narwhals DataFrame:
 - `.select(names)` (Ibis and DuckDB), where `names` is a list of (string) column names. This is useful for
   selecting columns before converting to another library.
 
-### Extending Narwhals
+## Extending Narwhals
 
 If you want your own library to be recognised too, you're welcome open a PR (with tests)!.
 Alternatively, if you can't do that (for example, if you library is closed-source), see
diff --git a/docs/index.md b/docs/index.md
index e9fe0217..7f88568a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -28,3 +28,8 @@ Anyone wishing to write a library/application/service which consumes dataframes,
 completely dataframe-agnostic.
 
 Let's get started!
+
+## Roadmap
+
+See [roadmap discussion on GitHub](https://github.com/narwhals-dev/narwhals/discussions/1370)
+for an up-to-date plan of future work.
diff --git a/docs/installation.md b/docs/installation.md
index 25c014ba..1ff9d5db 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -9,7 +9,7 @@
     If you haven't, you can follow our [_setting up your environment_](https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md#option-1-use-uv-recommended) guide.
     Then, run:
 
-    ```console
+    ```terminal
     uv pip install narwhals
     ```
 
@@ -19,18 +19,20 @@
 
     Then, run:
 
-    ```console
+    ```terminal
     python -m pip install narwhals
     ```
 
 ### Verifying the Installation
 
 To verify the installation, start the Python REPL and execute:
+
 ```python
 >>> import narwhals
 >>> narwhals.__version__
 '1.14.3'
 ```
+
 If you see the version number, then the installation was successful!
 
 ## Quick start
@@ -60,7 +62,7 @@ import narwhals as nw
 from narwhals.typing import IntoFrame
 
 
-def my_function(df_native: IntoFrame) -> list[str]:
+def agnostic_get_columns(df_native: IntoFrame) -> list[str]:
     df = nw.from_native(df_native)
     column_names = df.columns
     return column_names
@@ -72,17 +74,22 @@ df_polars = pl.DataFrame(data)
 table_pa = pa.table(data)
 
 print("pandas output")
-print(my_function(df_pandas))
+print(agnostic_get_columns(df_pandas))
 
 print("Polars output")
-print(my_function(df_polars))
+print(agnostic_get_columns(df_polars))
 
 print("PyArrow output")
-print(my_function(table_pa))
+print(agnostic_get_columns(table_pa))
 ```
 
-If you run `python t.py` then your output should look like the above. This is the simplest possible example of a dataframe-agnostic
-function - as we'll soon see, we can do much more advanced things.
+If you run `python t.py` then your output should look like the above.
+This is the simplest possible example of a dataframe-agnostic function - as we'll soon
+see, we can do much more advanced things.
+
 Let's learn about what you just did, and what Narwhals can do for you!
 
-Note: these examples are only using pandas, Polars and PyArrow. Please see the following to find the [supported libraries](extending.md).
+!!! info
+
+    These examples are using pandas, Polars and PyArrow, however Narwhals supports
+    other dataframe libraries (See [supported libraries](extending.md)).
diff --git a/docs/resources.md b/docs/resources.md
new file mode 100644
index 00000000..36369bf2
--- /dev/null
+++ b/docs/resources.md
@@ -0,0 +1,47 @@
+# Resources
+
+## Sponsors and institutional partners
+
+Narwhals is 100% independent, community-driven, and community-owned.
+We are extremely grateful to the following organisations for having
+provided some funding / development time:
+
+- [Quansight Labs](https://labs.quansight.org)
+- [Quansight Futures](https://www.qi.ventures)
+- [OpenTeams](https://www.openteams.com)
+- [POSSEE initiative](https://possee.org)
+- [BYU-Idaho](https://www.byui.edu)
+
+If you contribute to Narwhals on your organization's time, please let us know. We'd be happy to add your employer
+to this list!
+
+## Appears on
+
+Narwhals has been featured in several talks, podcasts, and blog posts:
+
+- [Talk Python to me Podcast](https://youtu.be/FSH7BZ0tuE0)
+  Ahoy, Narwhals are bridging the data science APIs
+
+- [Python Bytes Podcast](https://www.youtube.com/live/N7w_ESVW40I?si=y-wN1uCsAuJOKlOT&t=382)
+  Episode 402, topic #2
+
+- [Super Data Science: ML & AI Podcast](https://www.youtube.com/watch?v=TeG4U8R0U8U)  
+  Narwhals: For Pandas-to-Polars DataFrame Compatibility
+
+- [Sample Space Podcast | probabl](https://youtu.be/8hYdq4sWbbQ?si=WG0QP1CZ6gkFf18b)  
+  How Narwhals has many end users ... that never use it directly. - Marco Gorelli
+
+- [The Real Python Podcast](https://www.youtube.com/watch?v=w5DFZbFYzCM)
+  Narwhals: Expanding DataFrame Compatibility Between Libraries
+
+- [Pycon Lithuania](https://www.youtube.com/watch?v=-mdx7Cn6_6E)  
+  Marco Gorelli - DataFrame interoperatiblity - what's been achieved, and what comes next?
+
+- [Pycon Italy](https://www.youtube.com/watch?v=3IqUli9XsmQ)  
+  How you can write a dataframe-agnostic library - Marco Gorelli
+
+- [Polars Blog Post](https://pola.rs/posts/lightweight_plotting/)  
+  Polars has a new lightweight plotting backend
+
+- [Quansight Labs blog post (w/ Scikit-Lego)](https://labs.quansight.org/blog/scikit-lego-narwhals)  
+  How Narwhals and scikit-lego came together to achieve dataframe-agnosticism
diff --git a/docs/why.md b/docs/why.md
index 4ec605d1..3a0959e2 100644
--- a/docs/why.md
+++ b/docs/why.md
@@ -12,7 +12,9 @@ print(3 in pd.Series([1, 2, 3]))
 print(3 in pl.Series([1, 2, 3]))
 ```
 
-Try it out and see ;) Spoiler alert: they don't. pandas checks if `3` is in the index,
+Try it out and see 😉
+
+Spoiler alert: they don't. pandas checks if `3` is in the index,
 Polars checks if it's in the values.
 
 For another example, try running the code below - note how the outputs have different column names after the join!
@@ -30,10 +32,11 @@ print(pd_left_merge.columns)
 print(pl_left_merge.columns)
 ```
 
-There are several such subtle difference between the libraries. Writing dataframe-agnostic code is hard!
+There are several such subtle difference between the libraries. **Writing dataframe-agnostic code is hard!**
+
 But by having a unified, simple, and predictable API, you can focus on behaviour rather than on subtle
 implementation differences.
 
 Furthermore, both pandas and Polars frequently deprecate behaviour. Narwhals handles this for you by
-testing against nightly builds of both libraries and handling backwards compatibility internally 
+testing against nightly builds of both libraries and handling backwards compatibility internally
 (so you don't have to!).
diff --git a/mkdocs.yml b/mkdocs.yml
index d7f422f5..abbbd5fc 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -16,11 +16,12 @@ nav:
     - pandas_like_concepts/user_warning.md
     - pandas_like_concepts/column_names.md
     - pandas_like_concepts/boolean.md
-  - overhead.md
-  - backcompat.md
-  - extending.md
-  - how_it_works.md
-  - Roadmap and related projects: roadmap_and_related.md
+  - Overhead: overhead.md
+  - Perfect backwards compatibility policy: backcompat.md
+  - Supported libraries and extending Narwhals: extending.md
+  - How it works: how_it_works.md
+  - Ecosystem: ecosystem.md
+  - Resources: resources.md
   - API Completeness:
     - api-completeness/index.md
     - Supported DataFrame methods: api-completeness/dataframe.md
@@ -127,3 +128,4 @@ extra_javascript:
 
 extra_css:
   - https://unpkg.com/katex@0/dist/katex.min.css
+  - css/extra.css
\ No newline at end of file
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 563d918c..ea80f752 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -26,6 +26,7 @@ if TYPE_CHECKING:
     from types import ModuleType
 
     import numpy as np
+    import pandas as pd
     import pyarrow as pa
     from typing_extensions import Self
 
@@ -40,7 +41,7 @@ if TYPE_CHECKING:
 class ArrowDataFrame:
     # --- not in the spec ---
     def __init__(
-        self,
+        self: Self,
         native_dataframe: pa.Table,
         *,
         backend_version: tuple[int, ...],
@@ -51,7 +52,7 @@ class ArrowDataFrame:
         self._backend_version = backend_version
         self._dtypes = dtypes
 
-    def __narwhals_namespace__(self) -> ArrowNamespace:
+    def __narwhals_namespace__(self: Self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
         return ArrowNamespace(backend_version=self._backend_version, dtypes=self._dtypes)
@@ -63,64 +64,62 @@ class ArrowDataFrame:
         msg = f"Expected pyarrow, got: {type(self._implementation)}"  # pragma: no cover
         raise AssertionError(msg)
 
-    def __narwhals_dataframe__(self) -> Self:
+    def __narwhals_dataframe__(self: Self) -> Self:
         return self
 
-    def __narwhals_lazyframe__(self) -> Self:
+    def __narwhals_lazyframe__(self: Self) -> Self:
         return self
 
-    def _change_dtypes(self, dtypes: DTypes) -> Self:
+    def _change_dtypes(self: Self, dtypes: DTypes) -> Self:
         return self.__class__(
             self._native_frame, backend_version=self._backend_version, dtypes=dtypes
         )
 
-    def _from_native_frame(self, df: pa.Table) -> Self:
+    def _from_native_frame(self: Self, df: pa.Table) -> Self:
         return self.__class__(
             df, backend_version=self._backend_version, dtypes=self._dtypes
         )
 
     @property
-    def shape(self) -> tuple[int, int]:
+    def shape(self: Self) -> tuple[int, int]:
         return self._native_frame.shape  # type: ignore[no-any-return]
 
-    def __len__(self) -> int:
+    def __len__(self: Self) -> int:
         return len(self._native_frame)
 
-    def row(self, index: int) -> tuple[Any, ...]:
+    def row(self: Self, index: int) -> tuple[Any, ...]:
         return tuple(col[index] for col in self._native_frame)
 
     @overload
     def rows(
-        self,
+        self: Self,
         *,
         named: Literal[True],
     ) -> list[dict[str, Any]]: ...
 
     @overload
     def rows(
-        self,
+        self: Self,
         *,
-        named: Literal[False] = False,
+        named: Literal[False],
     ) -> list[tuple[Any, ...]]: ...
     @overload
     def rows(
-        self,
+        self: Self,
         *,
         named: bool,
     ) -> list[tuple[Any, ...]] | list[dict[str, Any]]: ...
 
-    def rows(
-        self, *, named: bool = False
-    ) -> list[tuple[Any, ...]] | list[dict[str, Any]]:
+    def rows(self: Self, *, named: bool) -> list[tuple[Any, ...]] | list[dict[str, Any]]:
         if not named:
-            return list(self.iter_rows(named=False))  # type: ignore[return-value]
+            return list(self.iter_rows(named=False, buffer_size=512))  # type: ignore[return-value]
         return self._native_frame.to_pylist()  # type: ignore[no-any-return]
 
     def iter_rows(
-        self,
+        self: Self,
         *,
-        named: bool = False,
-        buffer_size: int = 512,
+        named: bool,
+        buffer_size: int,
     ) -> Iterator[tuple[Any, ...]] | Iterator[dict[str, Any]]:
         df = self._native_frame
         num_rows = df.num_rows
@@ -133,7 +132,7 @@ class ArrowDataFrame:
             for i in range(0, num_rows, buffer_size):
                 yield from df[i : i + buffer_size].to_pylist()
 
-    def get_column(self, name: str) -> ArrowSeries:
+    def get_column(self: Self, name: str) -> ArrowSeries:
         from narwhals._arrow.series import ArrowSeries
 
         if not isinstance(name, str):
@@ -147,26 +146,26 @@ class ArrowDataFrame:
             dtypes=self._dtypes,
         )
 
-    def __array__(self, dtype: Any = None, copy: bool | None = None) -> np.ndarray:
+    def __array__(self: Self, dtype: Any, copy: bool | None) -> np.ndarray:
         return self._native_frame.__array__(dtype, copy=copy)
 
     @overload
-    def __getitem__(self, item: tuple[Sequence[int], str | int]) -> ArrowSeries: ...  # type: ignore[overload-overlap]
+    def __getitem__(self: Self, item: tuple[Sequence[int], str | int]) -> ArrowSeries: ...  # type: ignore[overload-overlap]
 
     @overload
-    def __getitem__(self, item: Sequence[int]) -> ArrowDataFrame: ...
+    def __getitem__(self: Self, item: Sequence[int]) -> ArrowDataFrame: ...
 
     @overload
-    def __getitem__(self, item: str) -> ArrowSeries: ...
+    def __getitem__(self: Self, item: str) -> ArrowSeries: ...
 
     @overload
-    def __getitem__(self, item: slice) -> ArrowDataFrame: ...
+    def __getitem__(self: Self, item: slice) -> ArrowDataFrame: ...
 
     @overload
-    def __getitem__(self, item: tuple[slice, slice]) -> ArrowDataFrame: ...
+    def __getitem__(self: Self, item: tuple[slice, slice]) -> ArrowDataFrame: ...
 
     def __getitem__(
-        self,
+        self: Self,
         item: (
             str
             | slice
@@ -274,22 +273,22 @@ class ArrowDataFrame:
             raise TypeError(msg)
 
     @property
-    def schema(self) -> dict[str, DType]:
+    def schema(self: Self) -> dict[str, DType]:
         schema = self._native_frame.schema
         return {
             name: native_to_narwhals_dtype(dtype, self._dtypes)
             for name, dtype in zip(schema.names, schema.types)
         }
 
-    def collect_schema(self) -> dict[str, DType]:
+    def collect_schema(self: Self) -> dict[str, DType]:
         return self.schema
 
     @property
-    def columns(self) -> list[str]:
+    def columns(self: Self) -> list[str]:
         return self._native_frame.schema.names  # type: ignore[no-any-return]
 
     def select(
-        self,
+        self: Self,
         *exprs: IntoArrowExpr,
         **named_exprs: IntoArrowExpr,
     ) -> Self:
@@ -307,7 +306,7 @@ class ArrowDataFrame:
         return self._from_native_frame(df)
 
     def with_columns(
-        self,
+        self: Self,
         *exprs: IntoArrowExpr,
         **named_exprs: IntoArrowExpr,
     ) -> Self:
@@ -336,16 +335,16 @@ class ArrowDataFrame:
 
         return self._from_native_frame(native_frame)
 
-    def group_by(self, *keys: str, drop_null_keys: bool) -> ArrowGroupBy:
+    def group_by(self: Self, *keys: str, drop_null_keys: bool) -> ArrowGroupBy:
         from narwhals._arrow.group_by import ArrowGroupBy
 
         return ArrowGroupBy(self, list(keys), drop_null_keys=drop_null_keys)
 
     def join(
-        self,
+        self: Self,
         other: Self,
         *,
-        how: Literal["left", "inner", "outer", "cross", "anti", "semi"] = "inner",
+        how: Literal["left", "inner", "outer", "cross", "anti", "semi"],
         left_on: str | list[str] | None,
         right_on: str | list[str] | None,
         suffix: str,
@@ -386,18 +385,18 @@ class ArrowDataFrame:
         )
 
     def join_asof(
-        self,
+        self: Self,
         other: Self,
         *,
-        left_on: str | None = None,
-        right_on: str | None = None,
-        on: str | None = None,
-        by_left: str | list[str] | None = None,
-        by_right: str | list[str] | None = None,
-        by: str | list[str] | None = None,
-        strategy: Literal["backward", "forward", "nearest"] = "backward",
+        left_on: str | None,
+        right_on: str | None,
+        on: str | None,
+        by_left: str | list[str] | None,
+        by_right: str | list[str] | None,
+        by: str | list[str] | None,
+        strategy: Literal["backward", "forward", "nearest"],
     ) -> Self:
-        msg = "join_asof is not yet supported on PyArrow tables"
+        msg = "join_asof is not yet supported on PyArrow tables"  # pragma: no cover
         raise NotImplementedError(msg)
 
     def drop(self: Self, columns: list[str], strict: bool) -> Self:  # noqa: FBT001
@@ -414,7 +413,7 @@ class ArrowDataFrame:
         return self.filter(~plx.any_horizontal(plx.col(*subset).is_null()))
 
     def sort(
-        self,
+        self: Self,
         by: str | Iterable[str],
         *more_by: str,
         descending: bool | Sequence[bool],
@@ -436,15 +435,23 @@ class ArrowDataFrame:
 
         return self._from_native_frame(df.sort_by(sorting, null_placement=null_placement))
 
-    def to_pandas(self) -> Any:
+    def to_pandas(self: Self) -> pd.DataFrame:
         return self._native_frame.to_pandas()
 
-    def to_numpy(self) -> Any:
+    def to_numpy(self: Self) -> np.ndarray:
         import numpy as np  # ignore-banned-import
 
         return np.column_stack([col.to_numpy() for col in self._native_frame.columns])
 
-    def to_dict(self, *, as_series: bool) -> Any:
+    @overload
+    def to_dict(self: Self, *, as_series: Literal[True]) -> dict[str, ArrowSeries]: ...
+
+    @overload
+    def to_dict(self: Self, *, as_series: Literal[False]) -> dict[str, list[Any]]: ...
+
+    def to_dict(
+        self: Self, *, as_series: bool
+    ) -> dict[str, ArrowSeries] | dict[str, list[Any]]:
         df = self._native_frame
 
         names_and_values = zip(df.column_names, df.columns)
@@ -463,7 +470,7 @@ class ArrowDataFrame:
         else:
             return {name: col.to_pylist() for name, col in names_and_values}
 
-    def with_row_index(self, name: str) -> Self:
+    def with_row_index(self: Self, name: str) -> Self:
         import pyarrow as pa  # ignore-banned-import()
 
         df = self._native_frame
@@ -471,7 +478,7 @@ class ArrowDataFrame:
         row_indices = pa.array(range(df.num_rows))
         return self._from_native_frame(df.append_column(name, row_indices))
 
-    def filter(self, *predicates: IntoArrowExpr, **constraints: Any) -> Self:
+    def filter(self: Self, *predicates: IntoArrowExpr, **constraints: Any) -> Self:
         if (
             len(predicates) == 1
             and isinstance(predicates[0], list)
@@ -490,7 +497,7 @@ class ArrowDataFrame:
             mask = expr._call(self)[0]._native_series
         return self._from_native_frame(self._native_frame.filter(mask))
 
-    def null_count(self) -> Self:
+    def null_count(self: Self) -> Self:
         import pyarrow as pa  # ignore-banned-import()
 
         df = self._native_frame
@@ -500,7 +507,7 @@ class ArrowDataFrame:
             pa.table({name: [col.null_count] for name, col in names_and_values})
         )
 
-    def head(self, n: int) -> Self:
+    def head(self: Self, n: int) -> Self:
         df = self._native_frame
         if n >= 0:
             return self._from_native_frame(df.slice(0, n))
@@ -508,7 +515,7 @@ class ArrowDataFrame:
             num_rows = df.num_rows
             return self._from_native_frame(df.slice(0, max(0, num_rows + n)))
 
-    def tail(self, n: int) -> Self:
+    def tail(self: Self, n: int) -> Self:
         df = self._native_frame
         if n >= 0:
             num_rows = df.num_rows
@@ -516,24 +523,24 @@ class ArrowDataFrame:
         else:
             return self._from_native_frame(df.slice(abs(n)))
 
-    def lazy(self) -> Self:
+    def lazy(self: Self) -> Self:
         return self
 
-    def collect(self) -> ArrowDataFrame:
+    def collect(self: Self) -> ArrowDataFrame:
         return ArrowDataFrame(
             self._native_frame,
             backend_version=self._backend_version,
             dtypes=self._dtypes,
         )
 
-    def clone(self) -> Self:
+    def clone(self: Self) -> Self:
         msg = "clone is not yet supported on PyArrow tables"
         raise NotImplementedError(msg)
 
     def is_empty(self: Self) -> bool:
         return self.shape[0] == 0
 
-    def item(self: Self, row: int | None = None, column: int | str | None = None) -> Any:
+    def item(self: Self, row: int | None, column: int | str | None) -> Any:
         if row is None and column is None:
             if self.shape != (1, 1):
                 msg = (
@@ -551,17 +558,17 @@ class ArrowDataFrame:
         _col = self.columns.index(column) if isinstance(column, str) else column
         return self._native_frame[_col][row]
 
-    def rename(self, mapping: dict[str, str]) -> Self:
+    def rename(self: Self, mapping: dict[str, str]) -> Self:
         df = self._native_frame
         new_cols = [mapping.get(c, c) for c in df.column_names]
         return self._from_native_frame(df.rename_columns(new_cols))
 
-    def write_parquet(self, file: Any) -> Any:
+    def write_parquet(self: Self, file: Any) -> None:
         import pyarrow.parquet as pp  # ignore-banned-import
 
         pp.write_table(self._native_frame, file)
 
-    def write_csv(self, file: Any) -> Any:
+    def write_csv(self: Self, file: Any) -> Any:
         import pyarrow as pa  # ignore-banned-import
         import pyarrow.csv as pa_csv  # ignore-banned-import
 
@@ -619,8 +626,8 @@ class ArrowDataFrame:
         self: Self,
         subset: str | list[str] | None,
         *,
-        keep: Literal["any", "first", "last", "none"] = "any",
-        maintain_order: bool = False,
+        keep: Literal["any", "first", "last", "none"],
+        maintain_order: bool,
     ) -> Self:
         # The param `maintain_order` is only here for compatibility with the Polars API
         # and has no effect on the output.
@@ -654,16 +661,16 @@ class ArrowDataFrame:
     def gather_every(self: Self, n: int, offset: int = 0) -> Self:
         return self._from_native_frame(self._native_frame[offset::n])
 
-    def to_arrow(self: Self) -> Any:
+    def to_arrow(self: Self) -> pa.Table:
         return self._native_frame
 
     def sample(
         self: Self,
-        n: int | None = None,
+        n: int | None,
         *,
-        fraction: float | None = None,
-        with_replacement: bool = False,
-        seed: int | None = None,
+        fraction: float | None,
+        with_replacement: bool,
+        seed: int | None,
     ) -> Self:
         import numpy as np  # ignore-banned-import
         import pyarrow.compute as pc  # ignore-banned-import()
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 4c04a182..1cb0a068 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
 
 class ArrowExpr:
     def __init__(
-        self,
+        self: Self,
         call: Callable[[ArrowDataFrame], list[ArrowSeries]],
         *,
         depth: int,
@@ -46,7 +46,7 @@ class ArrowExpr:
         self._backend_version = backend_version
         self._dtypes = dtypes
 
-    def __repr__(self) -> str:  # pragma: no cover
+    def __repr__(self: Self) -> str:  # pragma: no cover
         return (
             f"ArrowExpr("
             f"depth={self._depth}, "
@@ -122,151 +122,160 @@ class ArrowExpr:
             dtypes=dtypes,
         )
 
-    def __narwhals_namespace__(self) -> ArrowNamespace:
+    def __narwhals_namespace__(self: Self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
         return ArrowNamespace(backend_version=self._backend_version, dtypes=self._dtypes)
 
-    def __narwhals_expr__(self) -> None: ...
+    def __narwhals_expr__(self: Self) -> None: ...
 
-    def __eq__(self, other: ArrowExpr | Any) -> Self:  # type: ignore[override]
+    def __eq__(self: Self, other: ArrowExpr | Any) -> Self:  # type: ignore[override]
         return reuse_series_implementation(self, "__eq__", other=other)
 
-    def __ne__(self, other: ArrowExpr | Any) -> Self:  # type: ignore[override]
+    def __ne__(self: Self, other: ArrowExpr | Any) -> Self:  # type: ignore[override]
         return reuse_series_implementation(self, "__ne__", other=other)
 
-    def __ge__(self, other: ArrowExpr | Any) -> Self:
+    def __ge__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__ge__", other=other)
 
-    def __gt__(self, other: ArrowExpr | Any) -> Self:
+    def __gt__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__gt__", other=other)
 
-    def __le__(self, other: ArrowExpr | Any) -> Self:
+    def __le__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__le__", other=other)
 
-    def __lt__(self, other: ArrowExpr | Any) -> Self:
+    def __lt__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__lt__", other=other)
 
-    def __and__(self, other: ArrowExpr | bool | Any) -> Self:
+    def __and__(self: Self, other: ArrowExpr | bool | Any) -> Self:
         return reuse_series_implementation(self, "__and__", other=other)
 
-    def __rand__(self, other: ArrowExpr | bool | Any) -> Self:
-        return reuse_series_implementation(self, "__rand__", other=other)
+    def __rand__(self: Self, other: ArrowExpr | bool | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__and__(self)  # type: ignore[return-value]
 
-    def __or__(self, other: ArrowExpr | bool | Any) -> Self:
+    def __or__(self: Self, other: ArrowExpr | bool | Any) -> Self:
         return reuse_series_implementation(self, "__or__", other=other)
 
-    def __ror__(self, other: ArrowExpr | bool | Any) -> Self:
-        return reuse_series_implementation(self, "__ror__", other=other)
+    def __ror__(self: Self, other: ArrowExpr | bool | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__or__(self)  # type: ignore[return-value]
 
-    def __add__(self, other: ArrowExpr | Any) -> Self:
+    def __add__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__add__", other)
 
-    def __radd__(self, other: ArrowExpr | Any) -> Self:
-        return reuse_series_implementation(self, "__radd__", other)
+    def __radd__(self: Self, other: ArrowExpr | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__add__(self)  # type: ignore[return-value]
 
-    def __sub__(self, other: ArrowExpr | Any) -> Self:
+    def __sub__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__sub__", other)
 
-    def __rsub__(self, other: ArrowExpr | Any) -> Self:
-        return reuse_series_implementation(self, "__rsub__", other)
+    def __rsub__(self: Self, other: ArrowExpr | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__sub__(self)  # type: ignore[return-value]
 
-    def __mul__(self, other: ArrowExpr | Any) -> Self:
+    def __mul__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__mul__", other)
 
-    def __rmul__(self, other: ArrowExpr | Any) -> Self:
-        return reuse_series_implementation(self, "__rmul__", other)
+    def __rmul__(self: Self, other: ArrowExpr | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__mul__(self)  # type: ignore[return-value]
 
-    def __pow__(self, other: ArrowExpr | Any) -> Self:
+    def __pow__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__pow__", other)
 
-    def __rpow__(self, other: ArrowExpr | Any) -> Self:
-        return reuse_series_implementation(self, "__rpow__", other)
+    def __rpow__(self: Self, other: ArrowExpr | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__pow__(self)  # type: ignore[return-value]
 
-    def __floordiv__(self, other: ArrowExpr | Any) -> Self:
+    def __floordiv__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__floordiv__", other)
 
-    def __rfloordiv__(self, other: ArrowExpr | Any) -> Self:
-        return reuse_series_implementation(self, "__rfloordiv__", other)
+    def __rfloordiv__(self: Self, other: ArrowExpr | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__floordiv__(self)  # type: ignore[return-value]
 
-    def __truediv__(self, other: ArrowExpr | Any) -> Self:
+    def __truediv__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__truediv__", other)
 
-    def __rtruediv__(self, other: ArrowExpr | Any) -> Self:
-        return reuse_series_implementation(self, "__rtruediv__", other)
+    def __rtruediv__(self: Self, other: ArrowExpr | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__truediv__(self)  # type: ignore[return-value]
 
-    def __mod__(self, other: ArrowExpr | Any) -> Self:
+    def __mod__(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__mod__", other)
 
-    def __rmod__(self, other: ArrowExpr | Any) -> Self:
-        return reuse_series_implementation(self, "__rmod__", other)
+    def __rmod__(self: Self, other: ArrowExpr | Any) -> Self:
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__mod__(self)  # type: ignore[return-value]
 
-    def __invert__(self) -> Self:
+    def __invert__(self: Self) -> Self:
         return reuse_series_implementation(self, "__invert__")
 
-    def len(self) -> Self:
+    def len(self: Self) -> Self:
         return reuse_series_implementation(self, "len", returns_scalar=True)
 
-    def filter(self, *predicates: IntoArrowExpr) -> Self:
+    def filter(self: Self, *predicates: IntoArrowExpr) -> Self:
         plx = self.__narwhals_namespace__()
         expr = plx.all_horizontal(*predicates)
         return reuse_series_implementation(self, "filter", other=expr)
 
-    def mean(self) -> Self:
+    def mean(self: Self) -> Self:
         return reuse_series_implementation(self, "mean", returns_scalar=True)
 
-    def median(self) -> Self:
+    def median(self: Self) -> Self:
         return reuse_series_implementation(self, "median", returns_scalar=True)
 
-    def count(self) -> Self:
+    def count(self: Self) -> Self:
         return reuse_series_implementation(self, "count", returns_scalar=True)
 
-    def n_unique(self) -> Self:
+    def n_unique(self: Self) -> Self:
         return reuse_series_implementation(self, "n_unique", returns_scalar=True)
 
-    def std(self, ddof: int = 1) -> Self:
+    def std(self: Self, ddof: int = 1) -> Self:
         return reuse_series_implementation(self, "std", ddof=ddof, returns_scalar=True)
 
     def skew(self: Self) -> Self:
         return reuse_series_implementation(self, "skew", returns_scalar=True)
 
-    def cast(self, dtype: DType) -> Self:
+    def cast(self: Self, dtype: DType) -> Self:
         return reuse_series_implementation(self, "cast", dtype)
 
-    def abs(self) -> Self:
+    def abs(self: Self) -> Self:
         return reuse_series_implementation(self, "abs")
 
-    def diff(self) -> Self:
+    def diff(self: Self) -> Self:
         return reuse_series_implementation(self, "diff")
 
     def cum_sum(self: Self, *, reverse: bool) -> Self:
         return reuse_series_implementation(self, "cum_sum", reverse=reverse)
 
-    def round(self, decimals: int) -> Self:
+    def round(self: Self, decimals: int) -> Self:
         return reuse_series_implementation(self, "round", decimals)
 
-    def any(self) -> Self:
+    def any(self: Self) -> Self:
         return reuse_series_implementation(self, "any", returns_scalar=True)
 
-    def min(self) -> Self:
+    def min(self: Self) -> Self:
         return reuse_series_implementation(self, "min", returns_scalar=True)
 
-    def max(self) -> Self:
+    def max(self: Self) -> Self:
         return reuse_series_implementation(self, "max", returns_scalar=True)
 
-    def all(self) -> Self:
+    def all(self: Self) -> Self:
         return reuse_series_implementation(self, "all", returns_scalar=True)
 
-    def sum(self) -> Self:
+    def sum(self: Self) -> Self:
         return reuse_series_implementation(self, "sum", returns_scalar=True)
 
-    def drop_nulls(self) -> Self:
+    def drop_nulls(self: Self) -> Self:
         return reuse_series_implementation(self, "drop_nulls")
 
-    def shift(self, n: int) -> Self:
+    def shift(self: Self, n: int) -> Self:
         return reuse_series_implementation(self, "shift", n)
 
-    def alias(self, name: str) -> Self:
+    def alias(self: Self, name: str) -> Self:
         # Define this one manually, so that we can
         # override `output_names` and not increase depth
         return self.__class__(
@@ -279,36 +288,36 @@ class ArrowExpr:
             dtypes=self._dtypes,
         )
 
-    def null_count(self) -> Self:
+    def null_count(self: Self) -> Self:
         return reuse_series_implementation(self, "null_count", returns_scalar=True)
 
-    def is_null(self) -> Self:
+    def is_null(self: Self) -> Self:
         return reuse_series_implementation(self, "is_null")
 
-    def is_between(self, lower_bound: Any, upper_bound: Any, closed: str) -> Self:
+    def is_between(self: Self, lower_bound: Any, upper_bound: Any, closed: str) -> Self:
         return reuse_series_implementation(
             self, "is_between", lower_bound, upper_bound, closed
         )
 
-    def head(self, n: int) -> Self:
+    def head(self: Self, n: int) -> Self:
         return reuse_series_implementation(self, "head", n)
 
-    def tail(self, n: int) -> Self:
+    def tail(self: Self, n: int) -> Self:
         return reuse_series_implementation(self, "tail", n)
 
-    def is_in(self, other: ArrowExpr | Any) -> Self:
+    def is_in(self: Self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "is_in", other)
 
-    def arg_true(self) -> Self:
+    def arg_true(self: Self) -> Self:
         return reuse_series_implementation(self, "arg_true")
 
     def sample(
         self: Self,
-        n: int | None = None,
+        n: int | None,
         *,
-        fraction: float | None = None,
-        with_replacement: bool = False,
-        seed: int | None = None,
+        fraction: float | None,
+        with_replacement: bool,
+        seed: int | None,
     ) -> Self:
         return reuse_series_implementation(
             self,
@@ -321,9 +330,9 @@ class ArrowExpr:
 
     def fill_null(
         self: Self,
-        value: Any | None = None,
-        strategy: Literal["forward", "backward"] | None = None,
-        limit: int | None = None,
+        value: Any | None,
+        strategy: Literal["forward", "backward"] | None,
+        limit: int | None,
     ) -> Self:
         return reuse_series_implementation(
             self, "fill_null", value=value, strategy=strategy, limit=limit
@@ -341,7 +350,7 @@ class ArrowExpr:
     def is_last_distinct(self: Self) -> Self:
         return reuse_series_implementation(self, "is_last_distinct")
 
-    def unique(self: Self, *, maintain_order: bool = False) -> Self:
+    def unique(self: Self, *, maintain_order: bool) -> Self:
         return reuse_series_implementation(self, "unique", maintain_order=maintain_order)
 
     def replace_strict(
@@ -351,13 +360,13 @@ class ArrowExpr:
             self, "replace_strict", old, new, return_dtype=return_dtype
         )
 
-    def sort(self: Self, *, descending: bool = False, nulls_last: bool = False) -> Self:
+    def sort(self: Self, *, descending: bool, nulls_last: bool) -> Self:
         return reuse_series_implementation(
             self, "sort", descending=descending, nulls_last=nulls_last
         )
 
     def quantile(
-        self,
+        self: Self,
         quantile: float,
         interpolation: Literal["nearest", "higher", "lower", "midpoint", "linear"],
     ) -> Self:
@@ -368,9 +377,7 @@ class ArrowExpr:
     def gather_every(self: Self, n: int, offset: int = 0) -> Self:
         return reuse_series_implementation(self, "gather_every", n=n, offset=offset)
 
-    def clip(
-        self: Self, lower_bound: Any | None = None, upper_bound: Any | None = None
-    ) -> Self:
+    def clip(self: Self, lower_bound: Any | None, upper_bound: Any | None) -> Self:
         return reuse_series_implementation(
             self, "clip", lower_bound=lower_bound, upper_bound=upper_bound
         )
@@ -406,7 +413,7 @@ class ArrowExpr:
     def map_batches(
         self: Self,
         function: Callable[[Any], Any],
-        return_dtype: DType | None = None,
+        return_dtype: DType | None,
     ) -> Self:
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
             input_series_list = self._call(df)
@@ -504,10 +511,10 @@ class ArrowExpr:
 
 
 class ArrowExprCatNamespace:
-    def __init__(self, expr: ArrowExpr) -> None:
+    def __init__(self: Self, expr: ArrowExpr) -> None:
         self._expr = expr
 
-    def get_categories(self) -> ArrowExpr:
+    def get_categories(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
             "cat",
@@ -595,19 +602,19 @@ class ArrowExprDateTimeNamespace:
 
 
 class ArrowExprStringNamespace:
-    def __init__(self, expr: ArrowExpr) -> None:
+    def __init__(self: Self, expr: ArrowExpr) -> None:
         self._expr = expr
 
-    def len_chars(self) -> ArrowExpr:
+    def len_chars(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(self._expr, "str", "len_chars")
 
     def replace(
-        self,
+        self: Self,
         pattern: str,
         value: str,
         *,
-        literal: bool = False,
-        n: int = 1,
+        literal: bool,
+        n: int,
     ) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
@@ -620,11 +627,11 @@ class ArrowExprStringNamespace:
         )
 
     def replace_all(
-        self,
+        self: Self,
         pattern: str,
         value: str,
         *,
-        literal: bool = False,
+        literal: bool,
     ) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
@@ -635,7 +642,7 @@ class ArrowExprStringNamespace:
             literal=literal,
         )
 
-    def strip_chars(self, characters: str | None = None) -> ArrowExpr:
+    def strip_chars(self: Self, characters: str | None) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
             "str",
@@ -643,7 +650,7 @@ class ArrowExprStringNamespace:
             characters,
         )
 
-    def starts_with(self, prefix: str) -> ArrowExpr:
+    def starts_with(self: Self, prefix: str) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
             "str",
@@ -651,7 +658,7 @@ class ArrowExprStringNamespace:
             prefix,
         )
 
-    def ends_with(self, suffix: str) -> ArrowExpr:
+    def ends_with(self: Self, suffix: str) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
             "str",
@@ -664,7 +671,7 @@ class ArrowExprStringNamespace:
             self._expr, "str", "contains", pattern, literal=literal
         )
 
-    def slice(self, offset: int, length: int | None = None) -> ArrowExpr:
+    def slice(self: Self, offset: int, length: int | None) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr, "str", "slice", offset, length
         )
@@ -677,14 +684,14 @@ class ArrowExprStringNamespace:
             format,
         )
 
-    def to_uppercase(self) -> ArrowExpr:
+    def to_uppercase(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
             "str",
             "to_uppercase",
         )
 
-    def to_lowercase(self) -> ArrowExpr:
+    def to_lowercase(self: Self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
             "str",
diff --git a/narwhals/_arrow/group_by.py b/narwhals/_arrow/group_by.py
index e5f0bb8c..6bf9fa88 100644
--- a/narwhals/_arrow/group_by.py
+++ b/narwhals/_arrow/group_by.py
@@ -13,6 +13,8 @@ from narwhals.utils import remove_prefix
 
 if TYPE_CHECKING:
     import pyarrow as pa
+    import pyarrow.compute as pc
+    from typing_extensions import Self
 
     from narwhals._arrow.dataframe import ArrowDataFrame
     from narwhals._arrow.expr import ArrowExpr
@@ -27,7 +29,9 @@ POLARS_TO_ARROW_AGGREGATIONS = {
 }
 
 
-def get_function_name_option(function_name: str) -> Any | None:
+def get_function_name_option(
+    function_name: str,
+) -> pc.CountOptions | pc.VarianceOptions | None:
     """Map specific pyarrow compute function to respective option to match polars behaviour."""
     import pyarrow.compute as pc  # ignore-banned-import
 
@@ -42,7 +46,7 @@ def get_function_name_option(function_name: str) -> Any | None:
 
 class ArrowGroupBy:
     def __init__(
-        self, df: ArrowDataFrame, keys: list[str], *, drop_null_keys: bool
+        self: Self, df: ArrowDataFrame, keys: list[str], *, drop_null_keys: bool
     ) -> None:
         import pyarrow as pa  # ignore-banned-import()
 
@@ -54,7 +58,7 @@ class ArrowGroupBy:
         self._grouped = pa.TableGroupBy(self._df._native_frame, list(self._keys))
 
     def agg(
-        self,
+        self: Self,
         *aggs: IntoArrowExpr,
         **named_aggs: IntoArrowExpr,
     ) -> ArrowDataFrame:
@@ -82,7 +86,7 @@ class ArrowGroupBy:
             self._df._from_native_frame,
         )
 
-    def __iter__(self) -> Iterator[tuple[Any, ArrowDataFrame]]:
+    def __iter__(self: Self) -> Iterator[tuple[Any, ArrowDataFrame]]:
         import pyarrow as pa  # ignore-banned-import
         import pyarrow.compute as pc  # ignore-banned-import
 
@@ -108,7 +112,7 @@ class ArrowGroupBy:
                     )
                     .select(*self._keys)
                     .head(1)
-                    .iter_rows()
+                    .iter_rows(named=False, buffer_size=512)
                 ),
                 t,
             )
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index d6737ce9..07fa7deb 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -23,6 +23,8 @@ from narwhals.utils import Implementation
 if TYPE_CHECKING:
     from typing import Callable
 
+    from typing_extensions import Self
+
     from narwhals._arrow.typing import IntoArrowExpr
     from narwhals.dtypes import DType
     from narwhals.typing import DTypes
@@ -30,7 +32,7 @@ if TYPE_CHECKING:
 
 class ArrowNamespace:
     def _create_expr_from_callable(
-        self,
+        self: Self,
         func: Callable[[ArrowDataFrame], list[ArrowSeries]],
         *,
         depth: int,
@@ -50,7 +52,7 @@ class ArrowNamespace:
             dtypes=self._dtypes,
         )
 
-    def _create_expr_from_series(self, series: ArrowSeries) -> ArrowExpr:
+    def _create_expr_from_series(self: Self, series: ArrowSeries) -> ArrowExpr:
         from narwhals._arrow.expr import ArrowExpr
 
         return ArrowExpr(
@@ -64,7 +66,7 @@ class ArrowNamespace:
         )
 
     def _create_series_from_scalar(
-        self, value: Any, *, reference_series: ArrowSeries
+        self: Self, value: Any, *, reference_series: ArrowSeries
     ) -> ArrowSeries:
         from narwhals._arrow.series import ArrowSeries
 
@@ -77,7 +79,7 @@ class ArrowNamespace:
             dtypes=self._dtypes,
         )
 
-    def _create_compliant_series(self, value: Any) -> ArrowSeries:
+    def _create_compliant_series(self: Self, value: Any) -> ArrowSeries:
         import pyarrow as pa  # ignore-banned-import()
 
         from narwhals._arrow.series import ArrowSeries
@@ -90,27 +92,27 @@ class ArrowNamespace:
         )
 
     # --- not in spec ---
-    def __init__(self, *, backend_version: tuple[int, ...], dtypes: DTypes) -> None:
+    def __init__(self: Self, *, backend_version: tuple[int, ...], dtypes: DTypes) -> None:
         self._backend_version = backend_version
         self._implementation = Implementation.PYARROW
         self._dtypes = dtypes
 
     # --- selection ---
-    def col(self, *column_names: str) -> ArrowExpr:
+    def col(self: Self, *column_names: str) -> ArrowExpr:
         from narwhals._arrow.expr import ArrowExpr
 
         return ArrowExpr.from_column_names(
             *column_names, backend_version=self._backend_version, dtypes=self._dtypes
         )
 
-    def nth(self, *column_indices: int) -> ArrowExpr:
+    def nth(self: Self, *column_indices: int) -> ArrowExpr:
         from narwhals._arrow.expr import ArrowExpr
 
         return ArrowExpr.from_column_indices(
             *column_indices, backend_version=self._backend_version, dtypes=self._dtypes
         )
 
-    def len(self) -> ArrowExpr:
+    def len(self: Self) -> ArrowExpr:
         # coverage bug? this is definitely hit
         return ArrowExpr(  # pragma: no cover
             lambda df: [
@@ -129,7 +131,7 @@ class ArrowNamespace:
             dtypes=self._dtypes,
         )
 
-    def all(self) -> ArrowExpr:
+    def all(self: Self) -> ArrowExpr:
         from narwhals._arrow.expr import ArrowExpr
         from narwhals._arrow.series import ArrowSeries
 
@@ -151,7 +153,7 @@ class ArrowNamespace:
             dtypes=self._dtypes,
         )
 
-    def lit(self, value: Any, dtype: DType | None) -> ArrowExpr:
+    def lit(self: Self, value: Any, dtype: DType | None) -> ArrowExpr:
         def _lit_arrow_series(_: ArrowDataFrame) -> ArrowSeries:
             arrow_series = ArrowSeries._from_iterable(
                 data=[value],
@@ -168,12 +170,12 @@ class ArrowNamespace:
             depth=0,
             function_name="lit",
             root_names=None,
-            output_names=[_lit_arrow_series.__name__],
+            output_names=["literal"],
             backend_version=self._backend_version,
             dtypes=self._dtypes,
         )
 
-    def all_horizontal(self, *exprs: IntoArrowExpr) -> ArrowExpr:
+    def all_horizontal(self: Self, *exprs: IntoArrowExpr) -> ArrowExpr:
         parsed_exprs = parse_into_exprs(*exprs, namespace=self)
 
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
@@ -188,7 +190,7 @@ class ArrowNamespace:
             output_names=reduce_output_names(parsed_exprs),
         )
 
-    def any_horizontal(self, *exprs: IntoArrowExpr) -> ArrowExpr:
+    def any_horizontal(self: Self, *exprs: IntoArrowExpr) -> ArrowExpr:
         parsed_exprs = parse_into_exprs(*exprs, namespace=self)
 
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
@@ -203,11 +205,15 @@ class ArrowNamespace:
             output_names=reduce_output_names(parsed_exprs),
         )
 
-    def sum_horizontal(self, *exprs: IntoArrowExpr) -> ArrowExpr:
+    def sum_horizontal(self: Self, *exprs: IntoArrowExpr) -> ArrowExpr:
         parsed_exprs = parse_into_exprs(*exprs, namespace=self)
 
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
-            series = (s.fill_null(0) for _expr in parsed_exprs for s in _expr._call(df))
+            series = (
+                s.fill_null(0, strategy=None, limit=None)
+                for _expr in parsed_exprs
+                for s in _expr._call(df)
+            )
             return [reduce(lambda x, y: x + y, series)]
 
         return self._create_expr_from_callable(
@@ -218,11 +224,15 @@ class ArrowNamespace:
             output_names=reduce_output_names(parsed_exprs),
         )
 
-    def mean_horizontal(self, *exprs: IntoArrowExpr) -> IntoArrowExpr:
+    def mean_horizontal(self: Self, *exprs: IntoArrowExpr) -> IntoArrowExpr:
         parsed_exprs = parse_into_exprs(*exprs, namespace=self)
 
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
-            series = (s.fill_null(0) for _expr in parsed_exprs for s in _expr._call(df))
+            series = (
+                s.fill_null(0, strategy=None, limit=None)
+                for _expr in parsed_exprs
+                for s in _expr._call(df)
+            )
             non_na = (
                 1 - s.is_null().cast(self._dtypes.Int64())
                 for _expr in parsed_exprs
@@ -240,7 +250,7 @@ class ArrowNamespace:
             output_names=reduce_output_names(parsed_exprs),
         )
 
-    def min_horizontal(self, *exprs: IntoArrowExpr) -> ArrowExpr:
+    def min_horizontal(self: Self, *exprs: IntoArrowExpr) -> ArrowExpr:
         import pyarrow.compute as pc  # ignore-banned-import
 
         parsed_exprs = parse_into_exprs(*exprs, namespace=self)
@@ -268,7 +278,7 @@ class ArrowNamespace:
             output_names=reduce_output_names(parsed_exprs),
         )
 
-    def max_horizontal(self, *exprs: IntoArrowExpr) -> ArrowExpr:
+    def max_horizontal(self: Self, *exprs: IntoArrowExpr) -> ArrowExpr:
         import pyarrow.compute as pc  # ignore-banned-import
 
         parsed_exprs = parse_into_exprs(*exprs, namespace=self)
@@ -297,66 +307,63 @@ class ArrowNamespace:
         )
 
     def concat(
-        self,
+        self: Self,
         items: Iterable[ArrowDataFrame],
         *,
         how: Literal["horizontal", "vertical", "diagonal"],
     ) -> ArrowDataFrame:
-        dfs: list[Any] = [item._native_frame for item in items]
+        dfs = [item._native_frame for item in items]
+
+        if not dfs:
+            msg = "No dataframes to concatenate"  # pragma: no cover
+            raise AssertionError(msg)
 
         if how == "horizontal":
-            return ArrowDataFrame(
-                horizontal_concat(dfs),
-                backend_version=self._backend_version,
-                dtypes=self._dtypes,
-            )
-        if how == "vertical":
-            return ArrowDataFrame(
-                vertical_concat(dfs),
-                backend_version=self._backend_version,
-                dtypes=self._dtypes,
-            )
-        if how == "diagonal":
-            return ArrowDataFrame(
-                diagonal_concat(dfs, self._backend_version),
-                backend_version=self._backend_version,
-                dtypes=self._dtypes,
-            )
-        raise NotImplementedError
+            result_table = horizontal_concat(dfs)
+        elif how == "vertical":
+            result_table = vertical_concat(dfs)
+        elif how == "diagonal":
+            result_table = diagonal_concat(dfs, self._backend_version)
+        else:
+            raise NotImplementedError
+
+        return ArrowDataFrame(
+            result_table, backend_version=self._backend_version, dtypes=self._dtypes
+        )
 
-    def sum(self, *column_names: str) -> ArrowExpr:
+    def sum(self: Self, *column_names: str) -> ArrowExpr:
         return ArrowExpr.from_column_names(
             *column_names, backend_version=self._backend_version, dtypes=self._dtypes
         ).sum()
 
-    def mean(self, *column_names: str) -> ArrowExpr:
+    def mean(self: Self, *column_names: str) -> ArrowExpr:
         return ArrowExpr.from_column_names(
             *column_names, backend_version=self._backend_version, dtypes=self._dtypes
         ).mean()
 
-    def median(self, *column_names: str) -> ArrowExpr:
+    def median(self: Self, *column_names: str) -> ArrowExpr:
         return ArrowExpr.from_column_names(
             *column_names, backend_version=self._backend_version, dtypes=self._dtypes
         ).median()
 
-    def max(self, *column_names: str) -> ArrowExpr:
+    def max(self: Self, *column_names: str) -> ArrowExpr:
         return ArrowExpr.from_column_names(
             *column_names, backend_version=self._backend_version, dtypes=self._dtypes
         ).max()
 
-    def min(self, *column_names: str) -> ArrowExpr:
+    def min(self: Self, *column_names: str) -> ArrowExpr:
         return ArrowExpr.from_column_names(
             *column_names, backend_version=self._backend_version, dtypes=self._dtypes
         ).min()
 
     @property
-    def selectors(self) -> ArrowSelectorNamespace:
+    def selectors(self: Self) -> ArrowSelectorNamespace:
         return ArrowSelectorNamespace(
             backend_version=self._backend_version, dtypes=self._dtypes
         )
 
     def when(
-        self,
+        self: Self,
         *predicates: IntoArrowExpr,
     ) -> ArrowWhen:
         plx = self.__class__(backend_version=self._backend_version, dtypes=self._dtypes)
@@ -369,11 +376,11 @@ class ArrowNamespace:
         return ArrowWhen(condition, self._backend_version, dtypes=self._dtypes)
 
     def concat_str(
-        self,
+        self: Self,
         exprs: Iterable[IntoArrowExpr],
         *more_exprs: IntoArrowExpr,
-        separator: str = "",
-        ignore_nulls: bool = False,
+        separator: str,
+        ignore_nulls: bool,
     ) -> ArrowExpr:
         import pyarrow.compute as pc  # ignore-banned-import
 
@@ -426,7 +433,7 @@ class ArrowWhen:
         self._otherwise_value = otherwise_value
         self._dtypes = dtypes
 
-    def __call__(self, df: ArrowDataFrame) -> list[ArrowSeries]:
+    def __call__(self: Self, df: ArrowDataFrame) -> list[ArrowSeries]:
         import pyarrow as pa  # ignore-banned-import
         import pyarrow.compute as pc  # ignore-banned-import
 
@@ -486,7 +493,7 @@ class ArrowWhen:
                 )
             ]
 
-    def then(self, value: ArrowExpr | ArrowSeries | Any) -> ArrowThen:
+    def then(self: Self, value: ArrowExpr | ArrowSeries | Any) -> ArrowThen:
         self._then_value = value
 
         return ArrowThen(
@@ -502,7 +509,7 @@ class ArrowWhen:
 
 class ArrowThen(ArrowExpr):
     def __init__(
-        self,
+        self: Self,
         call: ArrowWhen,
         *,
         depth: int,
@@ -520,7 +527,7 @@ class ArrowThen(ArrowExpr):
         self._root_names = root_names
         self._output_names = output_names
 
-    def otherwise(self, value: ArrowExpr | ArrowSeries | Any) -> ArrowExpr:
+    def otherwise(self: Self, value: ArrowExpr | ArrowSeries | Any) -> ArrowExpr:
         # type ignore because we are setting the `_call` attribute to a
         # callable object of type `PandasWhen`, base class has the attribute as
         # only a `Callable`
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 88a91a39..ebfa5d6e 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -21,6 +21,8 @@ from narwhals.utils import generate_temporary_column_name
 if TYPE_CHECKING:
     from types import ModuleType
 
+    import numpy as np
+    import pandas as pd
     import pyarrow as pa
     from typing_extensions import Self
 
@@ -32,7 +34,7 @@ if TYPE_CHECKING:
 
 class ArrowSeries:
     def __init__(
-        self,
+        self: Self,
         native_series: pa.ChunkedArray,
         *,
         name: str,
@@ -45,7 +47,7 @@ class ArrowSeries:
         self._backend_version = backend_version
         self._dtypes = dtypes
 
-    def _change_dtypes(self, dtypes: DTypes) -> Self:
+    def _change_dtypes(self: Self, dtypes: DTypes) -> Self:
         return self.__class__(
             self._native_series,
             name=self._name,
@@ -53,7 +55,7 @@ class ArrowSeries:
             dtypes=dtypes,
         )
 
-    def _from_native_series(self, series: pa.ChunkedArray | pa.Array) -> Self:
+    def _from_native_series(self: Self, series: pa.ChunkedArray | pa.Array) -> Self:
         import pyarrow as pa  # ignore-banned-import()
 
         if isinstance(series, pa.Array):
@@ -83,194 +85,178 @@ class ArrowSeries:
             dtypes=dtypes,
         )
 
-    def __narwhals_namespace__(self) -> ArrowNamespace:
+    def __narwhals_namespace__(self: Self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
         return ArrowNamespace(backend_version=self._backend_version, dtypes=self._dtypes)
 
-    def __len__(self) -> int:
+    def __len__(self: Self) -> int:
         return len(self._native_series)
 
-    def __eq__(self, other: object) -> Self:  # type: ignore[override]
+    def __eq__(self: Self, other: object) -> Self:  # type: ignore[override]
         import pyarrow.compute as pc
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.equal(ser, other))
 
-    def __ne__(self, other: object) -> Self:  # type: ignore[override]
+    def __ne__(self: Self, other: object) -> Self:  # type: ignore[override]
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.not_equal(ser, other))
 
-    def __ge__(self, other: Any) -> Self:
+    def __ge__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.greater_equal(ser, other))
 
-    def __gt__(self, other: Any) -> Self:
+    def __gt__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.greater(ser, other))
 
-    def __le__(self, other: Any) -> Self:
+    def __le__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.less_equal(ser, other))
 
-    def __lt__(self, other: Any) -> Self:
+    def __lt__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.less(ser, other))
 
-    def __and__(self, other: Any) -> Self:
+    def __and__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.and_kleene(ser, other))
 
-    def __rand__(self, other: Any) -> Self:
+    def __rand__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.and_kleene(other, ser))
 
-    def __or__(self, other: Any) -> Self:
+    def __or__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.or_kleene(ser, other))
 
-    def __ror__(self, other: Any) -> Self:
+    def __ror__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.or_kleene(other, ser))
 
-    def __add__(self, other: Any) -> Self:
+    def __add__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        other = validate_column_comparand(other)
-        return self._from_native_series(pc.add(self._native_series, other))
+        ser, other = validate_column_comparand(self, other, self._backend_version)
+        return self._from_native_series(pc.add(ser, other))
 
-    def __radd__(self, other: Any) -> Self:
+    def __radd__(self: Self, other: Any) -> Self:
         return self + other  # type: ignore[no-any-return]
 
-    def __sub__(self, other: Any) -> Self:
+    def __sub__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        other = validate_column_comparand(other)
-        return self._from_native_series(pc.subtract(self._native_series, other))
+        ser, other = validate_column_comparand(self, other, self._backend_version)
+        return self._from_native_series(pc.subtract(ser, other))
 
-    def __rsub__(self, other: Any) -> Self:
+    def __rsub__(self: Self, other: Any) -> Self:
         return (self - other) * (-1)  # type: ignore[no-any-return]
 
-    def __mul__(self, other: Any) -> Self:
+    def __mul__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        other = validate_column_comparand(other)
-        return self._from_native_series(pc.multiply(self._native_series, other))
+        ser, other = validate_column_comparand(self, other, self._backend_version)
+        return self._from_native_series(pc.multiply(ser, other))
 
-    def __rmul__(self, other: Any) -> Self:
+    def __rmul__(self: Self, other: Any) -> Self:
         return self * other  # type: ignore[no-any-return]
 
-    def __pow__(self, other: Any) -> Self:
+    def __pow__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.power(ser, other))
 
-    def __rpow__(self, other: Any) -> Self:
+    def __rpow__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(pc.power(other, ser))
 
-    def __floordiv__(self, other: Any) -> Self:
-        ser = self._native_series
-        other = validate_column_comparand(other)
+    def __floordiv__(self: Self, other: Any) -> Self:
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(floordiv_compat(ser, other))
 
-    def __rfloordiv__(self, other: Any) -> Self:
-        ser = self._native_series
-        other = validate_column_comparand(other)
+    def __rfloordiv__(self: Self, other: Any) -> Self:
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         return self._from_native_series(floordiv_compat(other, ser))
 
-    def __truediv__(self, other: Any) -> Self:
+    def __truediv__(self: Self, other: Any) -> Self:
         import pyarrow as pa  # ignore-banned-import()
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         if not isinstance(other, (pa.Array, pa.ChunkedArray)):
             # scalar
             other = pa.scalar(other)
         return self._from_native_series(pc.divide(*cast_for_truediv(ser, other)))
 
-    def __rtruediv__(self, other: Any) -> Self:
+    def __rtruediv__(self: Self, other: Any) -> Self:
         import pyarrow as pa  # ignore-banned-import()
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         if not isinstance(other, (pa.Array, pa.ChunkedArray)):
             # scalar
             other = pa.scalar(other)
         return self._from_native_series(pc.divide(*cast_for_truediv(other, ser)))
 
-    def __mod__(self, other: Any) -> Self:
+    def __mod__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
         floor_div = (self // other)._native_series
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         res = pc.subtract(ser, pc.multiply(floor_div, other))
         return self._from_native_series(res)
 
-    def __rmod__(self, other: Any) -> Self:
+    def __rmod__(self: Self, other: Any) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        ser = self._native_series
-        other = validate_column_comparand(other)
         floor_div = (other // self)._native_series
+        ser, other = validate_column_comparand(self, other, self._backend_version)
         res = pc.subtract(other, pc.multiply(floor_div, ser))
         return self._from_native_series(res)
 
-    def __invert__(self) -> Self:
+    def __invert__(self: Self) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return self._from_native_series(pc.invert(self._native_series))
 
-    def len(self) -> int:
+    def len(self: Self) -> int:
         return len(self._native_series)
 
-    def filter(self, other: Any) -> Self:
+    def filter(self: Self, other: Any) -> Self:
         if not (isinstance(other, list) and all(isinstance(x, bool) for x in other)):
-            other = validate_column_comparand(other)
-        return self._from_native_series(self._native_series.filter(other))
+            ser, other = validate_column_comparand(self, other, self._backend_version)
+        else:
+            ser = self._native_series
+        return self._from_native_series(ser.filter(other))
 
-    def mean(self) -> int:
+    def mean(self: Self) -> int:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return pc.mean(self._native_series)  # type: ignore[no-any-return]
 
-    def median(self) -> int:
+    def median(self: Self) -> int:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         from narwhals.exceptions import InvalidOperationError
@@ -281,27 +267,27 @@ class ArrowSeries:
 
         return pc.approximate_median(self._native_series)  # type: ignore[no-any-return]
 
-    def min(self) -> int:
+    def min(self: Self) -> int:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return pc.min(self._native_series)  # type: ignore[no-any-return]
 
-    def max(self) -> int:
+    def max(self: Self) -> int:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return pc.max(self._native_series)  # type: ignore[no-any-return]
 
-    def sum(self) -> int:
+    def sum(self: Self) -> int:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return pc.sum(self._native_series)  # type: ignore[no-any-return]
 
-    def drop_nulls(self) -> ArrowSeries:
+    def drop_nulls(self: Self) -> ArrowSeries:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return self._from_native_series(pc.drop_null(self._native_series))
 
-    def shift(self, n: int) -> Self:
+    def shift(self: Self, n: int) -> Self:
         import pyarrow as pa  # ignore-banned-import()
 
         ca = self._native_series
@@ -314,7 +300,7 @@ class ArrowSeries:
             result = ca
         return self._from_native_series(result)
 
-    def std(self, ddof: int = 1) -> float:
+    def std(self: Self, ddof: int) -> float:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return pc.stddev(self._native_series, ddof=ddof)  # type: ignore[no-any-return]
@@ -337,12 +323,12 @@ class ArrowSeries:
             # Biased population skewness
             return pc.divide(m3, pc.power(m2, 1.5))  # type: ignore[no-any-return]
 
-    def count(self) -> int:
+    def count(self: Self) -> int:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return pc.count(self._native_series)  # type: ignore[no-any-return]
 
-    def n_unique(self) -> int:
+    def n_unique(self: Self) -> int:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         unique_values = pc.unique(self._native_series)
@@ -356,52 +342,53 @@ class ArrowSeries:
         raise AssertionError(msg)
 
     @property
-    def name(self) -> str:
+    def name(self: Self) -> str:
         return self._name
 
-    def __narwhals_series__(self) -> Self:
+    def __narwhals_series__(self: Self) -> Self:
         return self
 
     @overload
-    def __getitem__(self, idx: int) -> Any: ...
+    def __getitem__(self: Self, idx: int) -> Any: ...
 
     @overload
-    def __getitem__(self, idx: slice | Sequence[int]) -> Self: ...
+    def __getitem__(self: Self, idx: slice | Sequence[int]) -> Self: ...
 
-    def __getitem__(self, idx: int | slice | Sequence[int]) -> Any | Self:
+    def __getitem__(self: Self, idx: int | slice | Sequence[int]) -> Any | Self:
         if isinstance(idx, int):
             return self._native_series[idx]
         if isinstance(idx, Sequence):
             return self._from_native_series(self._native_series.take(idx))
         return self._from_native_series(self._native_series[idx])
 
-    def scatter(self, indices: int | Sequence[int], values: Any) -> Self:
+    def scatter(self: Self, indices: int | Sequence[int], values: Any) -> Self:
         import numpy as np  # ignore-banned-import
         import pyarrow as pa  # ignore-banned-import
         import pyarrow.compute as pc  # ignore-banned-import
 
-        ca = self._native_series
-        mask = np.zeros(len(ca), dtype=bool)
+        mask = np.zeros(self.len(), dtype=bool)
         mask[indices] = True
         if isinstance(values, self.__class__):
-            values = validate_column_comparand(values)
+            ser, values = validate_column_comparand(self, values, self._backend_version)
+        else:
+            ser = self._native_series
         if isinstance(values, pa.ChunkedArray):
             values = values.combine_chunks()
         if not isinstance(values, pa.Array):
             values = pa.array(values)
-        result = pc.replace_with_mask(ca, mask, values.take(indices))
+        result = pc.replace_with_mask(ser, mask, values.take(indices))
         return self._from_native_series(result)
 
-    def to_list(self) -> Any:
-        return self._native_series.to_pylist()
+    def to_list(self: Self) -> list[Any]:
+        return self._native_series.to_pylist()  # type: ignore[no-any-return]
 
-    def __array__(self, dtype: Any = None, copy: bool | None = None) -> Any:
+    def __array__(self: Self, dtype: Any = None, copy: bool | None = None) -> np.ndarray:
         return self._native_series.__array__(dtype=dtype, copy=copy)
 
-    def to_numpy(self) -> Any:
+    def to_numpy(self: Self) -> np.ndarray:
         return self._native_series.to_numpy()
 
-    def alias(self, name: str) -> Self:
+    def alias(self: Self, name: str) -> Self:
         return self.__class__(
             self._native_series,
             name=name,
@@ -413,7 +400,7 @@ class ArrowSeries:
     def dtype(self: Self) -> DType:
         return native_to_narwhals_dtype(self._native_series.type, self._dtypes)
 
-    def abs(self) -> Self:
+    def abs(self: Self) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return self._from_native_series(pc.abs(self._native_series))
@@ -429,26 +416,26 @@ class ArrowSeries:
         )
         return self._from_native_series(result)
 
-    def round(self, decimals: int) -> Self:
+    def round(self: Self, decimals: int) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return self._from_native_series(
             pc.round(self._native_series, decimals, round_mode="half_towards_infinity")
         )
 
-    def diff(self) -> Self:
+    def diff(self: Self) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return self._from_native_series(
             pc.pairwise_diff(self._native_series.combine_chunks())
         )
 
-    def any(self) -> bool:
+    def any(self: Self) -> bool:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return to_py_scalar(pc.any(self._native_series))  # type: ignore[no-any-return]
 
-    def all(self) -> bool:
+    def all(self: Self) -> bool:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return to_py_scalar(pc.all(self._native_series))  # type: ignore[no-any-return]
@@ -479,14 +466,14 @@ class ArrowSeries:
             raise AssertionError
         return self._from_native_series(res)
 
-    def is_empty(self) -> bool:
+    def is_empty(self: Self) -> bool:
         return len(self) == 0
 
-    def is_null(self) -> Self:
+    def is_null(self: Self) -> Self:
         ser = self._native_series
         return self._from_native_series(ser.is_null())
 
-    def cast(self, dtype: DType) -> Self:
+    def cast(self: Self, dtype: DType) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         ser = self._native_series
@@ -496,7 +483,7 @@ class ArrowSeries:
     def null_count(self: Self) -> int:
         return self._native_series.null_count  # type: ignore[no-any-return]
 
-    def head(self, n: int) -> Self:
+    def head(self: Self, n: int) -> Self:
         ser = self._native_series
         if n >= 0:
             return self._from_native_series(ser.slice(0, n))
@@ -504,7 +491,7 @@ class ArrowSeries:
             num_rows = len(ser)
             return self._from_native_series(ser.slice(0, max(0, num_rows + n)))
 
-    def tail(self, n: int) -> Self:
+    def tail(self: Self, n: int) -> Self:
         ser = self._native_series
         if n >= 0:
             num_rows = len(ser)
@@ -512,7 +499,7 @@ class ArrowSeries:
         else:
             return self._from_native_series(ser.slice(abs(n)))
 
-    def is_in(self, other: Any) -> Self:
+    def is_in(self: Self, other: Any) -> Self:
         import pyarrow as pa  # ignore-banned-import()
         import pyarrow.compute as pc  # ignore-banned-import()
 
@@ -520,7 +507,7 @@ class ArrowSeries:
         ser = self._native_series
         return self._from_native_series(pc.is_in(ser, value_set=value_set))
 
-    def arg_true(self) -> Self:
+    def arg_true(self: Self) -> Self:
         import numpy as np  # ignore-banned-import
 
         ser = self._native_series
@@ -592,11 +579,11 @@ class ArrowSeries:
 
     def sample(
         self: Self,
-        n: int | None = None,
+        n: int | None,
         *,
-        fraction: float | None = None,
-        with_replacement: bool = False,
-        seed: int | None = None,
+        fraction: float | None,
+        with_replacement: bool,
+        seed: int | None,
     ) -> Self:
         import numpy as np  # ignore-banned-import
         import pyarrow.compute as pc  # ignore-banned-import()
@@ -615,9 +602,9 @@ class ArrowSeries:
 
     def fill_null(
         self: Self,
-        value: Any | None = None,
-        strategy: Literal["forward", "backward"] | None = None,
-        limit: int | None = None,
+        value: Any | None,
+        strategy: Literal["forward", "backward"] | None,
+        limit: int | None,
     ) -> Self:
         import numpy as np  # ignore-banned-import
         import pyarrow as pa  # ignore-banned-import()
@@ -675,7 +662,7 @@ class ArrowSeries:
             df, backend_version=self._backend_version, dtypes=self._dtypes
         )
 
-    def to_pandas(self: Self) -> Any:
+    def to_pandas(self: Self) -> pd.Series:
         import pandas as pd  # ignore-banned-import()
 
         return pd.Series(self._native_series, name=self.name)
@@ -720,7 +707,7 @@ class ArrowSeries:
 
         return self._from_native_series(pc.is_in(row_number, last_distinct_index))
 
-    def is_sorted(self: Self, *, descending: bool = False) -> bool:
+    def is_sorted(self: Self, *, descending: bool) -> bool:
         if not isinstance(descending, bool):
             msg = f"argument 'descending' should be boolean, found {type(descending)}"
             raise TypeError(msg)
@@ -733,7 +720,7 @@ class ArrowSeries:
             result = pc.all(pc.less_equal(ser[:-1], ser[1:]))
         return to_py_scalar(result)  # type: ignore[no-any-return]
 
-    def unique(self: Self, *, maintain_order: bool = False) -> ArrowSeries:
+    def unique(self: Self, *, maintain_order: bool) -> ArrowSeries:
         # The param `maintain_order` is only here for compatibility with the Polars API
         # and has no effect on the output.
         import pyarrow.compute as pc  # ignore-banned-import()
@@ -755,14 +742,13 @@ class ArrowSeries:
         if result.is_null().sum() != self.is_null().sum():
             msg = (
                 "replace_strict did not replace all non-null values.\n\n"
-                f"The following did not get replaced: {self.filter(~self.is_null() & result.is_null()).unique().to_list()}"
+                "The following did not get replaced: "
+                f"{self.filter(~self.is_null() & result.is_null()).unique(maintain_order=False).to_list()}"
             )
             raise ValueError(msg)
         return result
 
-    def sort(
-        self: Self, *, descending: bool = False, nulls_last: bool = False
-    ) -> ArrowSeries:
+    def sort(self: Self, *, descending: bool, nulls_last: bool) -> ArrowSeries:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         series = self._native_series
@@ -774,9 +760,7 @@ class ArrowSeries:
 
         return self._from_native_series(pc.take(series, sorted_indices))
 
-    def to_dummies(
-        self: Self, *, separator: str = "_", drop_first: bool = False
-    ) -> ArrowDataFrame:
+    def to_dummies(self: Self, *, separator: str, drop_first: bool) -> ArrowDataFrame:
         import numpy as np  # ignore-banned-import
         import pyarrow as pa  # ignore-banned-import()
 
@@ -824,9 +808,7 @@ class ArrowSeries:
     def gather_every(self: Self, n: int, offset: int = 0) -> Self:
         return self._from_native_series(self._native_series[offset::n])
 
-    def clip(
-        self: Self, lower_bound: Any | None = None, upper_bound: Any | None = None
-    ) -> Self:
+    def clip(self: Self, lower_bound: Any | None, upper_bound: Any | None) -> Self:
         import pyarrow as pa  # ignore-banned-import()
         import pyarrow.compute as pc  # ignore-banned-import()
 
@@ -929,15 +911,20 @@ class ArrowSeries:
         else:
             padded_arr = self
 
-        cum_sum = padded_arr.cum_sum(reverse=False).fill_null(strategy="forward")
+        cum_sum = padded_arr.cum_sum(reverse=False).fill_null(
+            value=None, strategy="forward", limit=None
+        )
         rolling_sum = (
-            cum_sum - cum_sum.shift(window_size).fill_null(0)
+            cum_sum
+            - cum_sum.shift(window_size).fill_null(value=0, strategy=None, limit=None)
             if window_size != 0
             else cum_sum
         )
 
         valid_count = padded_arr.cum_count(reverse=False)
-        count_in_window = valid_count - valid_count.shift(window_size).fill_null(0)
+        count_in_window = valid_count - valid_count.shift(window_size).fill_null(
+            value=0, strategy=None, limit=None
+        )
 
         result = self._from_native_series(
             pc.if_else(
@@ -977,15 +964,20 @@ class ArrowSeries:
         else:
             padded_arr = self
 
-        cum_sum = padded_arr.cum_sum(reverse=False).fill_null(strategy="forward")
+        cum_sum = padded_arr.cum_sum(reverse=False).fill_null(
+            value=None, strategy="forward", limit=None
+        )
         rolling_sum = (
-            cum_sum - cum_sum.shift(window_size).fill_null(0)
+            cum_sum
+            - cum_sum.shift(window_size).fill_null(value=0, strategy=None, limit=None)
             if window_size != 0
             else cum_sum
         )
 
         valid_count = padded_arr.cum_count(reverse=False)
-        count_in_window = valid_count - valid_count.shift(window_size).fill_null(0)
+        count_in_window = valid_count - valid_count.shift(window_size).fill_null(
+            value=0, strategy=None, limit=None
+        )
 
         result = (
             self._from_native_series(
@@ -1005,19 +997,19 @@ class ArrowSeries:
         yield from self._native_series.__iter__()
 
     @property
-    def shape(self) -> tuple[int]:
+    def shape(self: Self) -> tuple[int]:
         return (len(self._native_series),)
 
     @property
-    def dt(self) -> ArrowSeriesDateTimeNamespace:
+    def dt(self: Self) -> ArrowSeriesDateTimeNamespace:
         return ArrowSeriesDateTimeNamespace(self)
 
     @property
-    def cat(self) -> ArrowSeriesCatNamespace:
+    def cat(self: Self) -> ArrowSeriesCatNamespace:
         return ArrowSeriesCatNamespace(self)
 
     @property
-    def str(self) -> ArrowSeriesStringNamespace:
+    def str(self: Self) -> ArrowSeriesStringNamespace:
         return ArrowSeriesStringNamespace(self)
 
 
@@ -1303,10 +1295,10 @@ class ArrowSeriesDateTimeNamespace:
 
 
 class ArrowSeriesCatNamespace:
-    def __init__(self, series: ArrowSeries) -> None:
+    def __init__(self: Self, series: ArrowSeries) -> None:
         self._arrow_series = series
 
-    def get_categories(self) -> ArrowSeries:
+    def get_categories(self: Self) -> ArrowSeries:
         import pyarrow as pa  # ignore-banned-import()
 
         ca = self._arrow_series._native_series
@@ -1322,7 +1314,7 @@ class ArrowSeriesStringNamespace:
     def __init__(self: Self, series: ArrowSeries) -> None:
         self._arrow_series = series
 
-    def len_chars(self) -> ArrowSeries:
+    def len_chars(self: Self) -> ArrowSeries:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return self._arrow_series._from_native_series(
@@ -1330,7 +1322,7 @@ class ArrowSeriesStringNamespace:
         )
 
     def replace(
-        self, pattern: str, value: str, *, literal: bool = False, n: int = 1
+        self: Self, pattern: str, value: str, *, literal: bool, n: int
     ) -> ArrowSeries:
         import pyarrow.compute as pc  # ignore-banned-import()
 
@@ -1345,11 +1337,11 @@ class ArrowSeriesStringNamespace:
         )
 
     def replace_all(
-        self, pattern: str, value: str, *, literal: bool = False
+        self: Self, pattern: str, value: str, *, literal: bool
     ) -> ArrowSeries:
         return self.replace(pattern, value, literal=literal, n=-1)
 
-    def strip_chars(self: Self, characters: str | None = None) -> ArrowSeries:
+    def strip_chars(self: Self, characters: str | None) -> ArrowSeries:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         whitespace = " \t\n\r\v\f"
@@ -1371,10 +1363,10 @@ class ArrowSeriesStringNamespace:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         return self._arrow_series._from_native_series(
-            pc.equal(self.slice(-len(suffix))._native_series, suffix)
+            pc.equal(self.slice(-len(suffix), None)._native_series, suffix)
         )
 
-    def contains(self: Self, pattern: str, *, literal: bool = False) -> ArrowSeries:
+    def contains(self: Self, pattern: str, *, literal: bool) -> ArrowSeries:
         import pyarrow.compute as pc  # ignore-banned-import()
 
         check_func = pc.match_substring if literal else pc.match_substring_regex
@@ -1382,10 +1374,10 @@ class ArrowSeriesStringNamespace:
             check_func(self._arrow_series._native_series, pattern)
         )
 
-    def slice(self: Self, offset: int, length: int | None = None) -> ArrowSeries:
+    def slice(self: Self, offset: int, length: int | None) -> ArrowSeries:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        stop = offset + length if length else None
+        stop = offset + length if length is not None else None
         return self._arrow_series._from_native_series(
             pc.utf8_slice_codeunits(
                 self._arrow_series._native_series, start=offset, stop=stop
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index 45377847..f8c25433 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -3,10 +3,12 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Sequence
+from typing import overload
 
 from narwhals.utils import isinstance_or_issubclass
 
 if TYPE_CHECKING:
+    import numpy as np
     import pyarrow as pa
 
     from narwhals._arrow.series import ArrowSeries
@@ -75,7 +77,7 @@ def native_to_narwhals_dtype(dtype: pa.DataType, dtypes: DTypes) -> DType:
     return dtypes.Unknown()  # pragma: no cover
 
 
-def narwhals_to_native_dtype(dtype: DType | type[DType], dtypes: DTypes) -> Any:
+def narwhals_to_native_dtype(dtype: DType | type[DType], dtypes: DTypes) -> pa.DataType:
     import pyarrow as pa  # ignore-banned-import
 
     if isinstance_or_issubclass(dtype, dtypes.Float64):
@@ -126,7 +128,9 @@ def narwhals_to_native_dtype(dtype: DType | type[DType], dtypes: DTypes) -> Any:
     raise AssertionError(msg)
 
 
-def validate_column_comparand(other: Any) -> Any:
+def validate_column_comparand(
+    lhs: ArrowSeries, rhs: Any, backend_version: tuple[int, ...]
+) -> tuple[pa.ChunkedArray, Any]:
     """Validate RHS of binary operation.
 
     If the comparison isn't supported, return `NotImplemented` so that the
@@ -138,27 +142,47 @@ def validate_column_comparand(other: Any) -> Any:
     from narwhals._arrow.dataframe import ArrowDataFrame
     from narwhals._arrow.series import ArrowSeries
 
-    if isinstance(other, list):
-        if len(other) > 1:
-            if hasattr(other[0], "__narwhals_expr__") or hasattr(
-                other[0], "__narwhals_series__"
+    # If `rhs` is the output of an expression evaluation, then it is
+    # a list of Series. So, we verify that that list is of length-1,
+    # and take the first (and only) element.
+    if isinstance(rhs, list):
+        if len(rhs) > 1:
+            if hasattr(rhs[0], "__narwhals_expr__") or hasattr(
+                rhs[0], "__narwhals_series__"
             ):
                 # e.g. `plx.all() + plx.all()`
                 msg = "Multi-output expressions (e.g. `nw.all()` or `nw.col('a', 'b')`) are not supported in this context"
                 raise ValueError(msg)
-            msg = (
-                f"Expected scalar value, Series, or Expr, got list of : {type(other[0])}"
-            )
+            msg = f"Expected scalar value, Series, or Expr, got list of : {type(rhs[0])}"
             raise ValueError(msg)
-        other = other[0]
-    if isinstance(other, ArrowDataFrame):
+        rhs = rhs[0]
+
+    if isinstance(rhs, ArrowDataFrame):
         return NotImplemented
-    if isinstance(other, ArrowSeries):
-        if len(other) == 1:
+
+    if isinstance(rhs, ArrowSeries):
+        if len(rhs) == 1:
             # broadcast
-            return other[0]
-        return other._native_series
-    return other
+            return lhs._native_series, rhs[0]
+        if len(lhs) == 1:
+            # broadcast
+            import numpy as np  # ignore-banned-import
+            import pyarrow as pa  # ignore-banned-import
+
+            fill_value = lhs[0]
+            if backend_version < (13,) and hasattr(fill_value, "as_py"):
+                fill_value = fill_value.as_py()
+            left_result = pa.chunked_array(
+                [
+                    pa.array(
+                        np.full(shape=rhs.len(), fill_value=fill_value),
+                        type=lhs._native_series.type,
+                    )
+                ]
+            )
+            return left_result, rhs._native_series
+        return lhs._native_series, rhs._native_series
+    return lhs._native_series, rhs
 
 
 def validate_dataframe_comparand(
@@ -177,7 +201,7 @@ def validate_dataframe_comparand(
             import pyarrow as pa  # ignore-banned-import
 
             value = other._native_series[0]
-            if backend_version < (13,) and hasattr(value, "as_py"):  # pragma: no cover
+            if backend_version < (13,) and hasattr(value, "as_py"):
                 value = value.as_py()
             return pa.array(np.full(shape=length, fill_value=value))
         return other._native_series
@@ -191,17 +215,13 @@ def validate_dataframe_comparand(
     raise AssertionError(msg)
 
 
-def horizontal_concat(dfs: list[Any]) -> Any:
+def horizontal_concat(dfs: list[pa.Table]) -> pa.Table:
     """Concatenate (native) DataFrames horizontally.
 
     Should be in namespace.
     """
     import pyarrow as pa  # ignore-banned-import
 
-    if not dfs:
-        msg = "No dataframes to concatenate"  # pragma: no cover
-        raise AssertionError(msg)
-
     names = [name for df in dfs for name in df.column_names]
 
     if len(set(names)) < len(names):  # pragma: no cover
@@ -212,15 +232,11 @@ def horizontal_concat(dfs: list[Any]) -> Any:
     return pa.Table.from_arrays(arrays, names=names)
 
 
-def vertical_concat(dfs: list[Any]) -> Any:
+def vertical_concat(dfs: list[pa.Table]) -> pa.Table:
     """Concatenate (native) DataFrames vertically.
 
     Should be in namespace.
     """
-    if not dfs:
-        msg = "No dataframes to concatenate"  # pragma: no cover
-        raise AssertionError(msg)
-
     cols_0 = dfs[0].column_names
     for i, df in enumerate(dfs[1:], start=1):
         cols_current = df.column_names
@@ -237,15 +253,11 @@ def vertical_concat(dfs: list[Any]) -> Any:
     return pa.concat_tables(dfs).combine_chunks()
 
 
-def diagonal_concat(dfs: list[Any], backend_version: tuple[int, ...]) -> Any:
+def diagonal_concat(dfs: list[pa.Table], backend_version: tuple[int, ...]) -> pa.Table:
     """Concatenate (native) DataFrames diagonally.
 
     Should be in namespace.
     """
-    if not dfs:
-        msg = "No dataframes to concatenate"  # pragma: no cover
-        raise AssertionError(msg)
-
     import pyarrow as pa  # ignore-banned-import
 
     kwargs = (
@@ -253,7 +265,6 @@ def diagonal_concat(dfs: list[Any], backend_version: tuple[int, ...]) -> Any:
         if backend_version < (14, 0, 0)
         else {"promote_options": "default"}  # type: ignore[dict-item]
     )
-
     return pa.concat_tables(dfs, **kwargs).combine_chunks()
 
 
@@ -332,7 +343,7 @@ def broadcast_series(series: list[ArrowSeries]) -> list[Any]:
         s_native = s._native_series
         if is_max_length_gt_1 and length == 1:
             value = s_native[0]
-            if s._backend_version < (13,) and hasattr(value, "as_py"):  # pragma: no cover
+            if s._backend_version < (13,) and hasattr(value, "as_py"):
                 value = value.as_py()
             reshaped.append(pa.array([value] * max_length, type=s_native.type))
         else:
@@ -341,12 +352,26 @@ def broadcast_series(series: list[ArrowSeries]) -> list[Any]:
     return reshaped
 
 
+@overload
+def convert_slice_to_nparray(num_rows: int, rows_slice: slice) -> np.ndarray: ...
+
+
+@overload
+def convert_slice_to_nparray(num_rows: int, rows_slice: int) -> int: ...
+
+
+@overload
 def convert_slice_to_nparray(
-    num_rows: int, rows_slice: slice | int | Sequence[int]
-) -> Any:
-    import numpy as np  # ignore-banned-import
+    num_rows: int, rows_slice: Sequence[int]
+) -> Sequence[int]: ...
+
 
+def convert_slice_to_nparray(
+    num_rows: int, rows_slice: slice | int | Sequence[int]
+) -> np.ndarray | int | Sequence[int]:
     if isinstance(rows_slice, slice):
+        import numpy as np  # ignore-banned-import
+
         return np.arange(num_rows)[rows_slice]
     else:
         return rows_slice
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index 58e73792..d4e07c2c 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -199,7 +199,7 @@ class DaskExpr:
             "__radd__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __sub__(self, other: Any) -> Self:
         return self._from_call(
@@ -215,7 +215,7 @@ class DaskExpr:
             "__rsub__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __mul__(self, other: Any) -> Self:
         return self._from_call(
@@ -231,7 +231,7 @@ class DaskExpr:
             "__rmul__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __truediv__(self, other: Any) -> Self:
         return self._from_call(
@@ -247,7 +247,7 @@ class DaskExpr:
             "__rtruediv__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __floordiv__(self, other: Any) -> Self:
         return self._from_call(
@@ -263,7 +263,7 @@ class DaskExpr:
             "__rfloordiv__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __pow__(self, other: Any) -> Self:
         return self._from_call(
@@ -279,7 +279,7 @@ class DaskExpr:
             "__rpow__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __mod__(self, other: Any) -> Self:
         return self._from_call(
@@ -295,7 +295,7 @@ class DaskExpr:
             "__rmod__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __eq__(self, other: DaskExpr) -> Self:  # type: ignore[override]
         return self._from_call(
@@ -353,13 +353,13 @@ class DaskExpr:
             returns_scalar=False,
         )
 
-    def __rand__(self, other: DaskExpr) -> Self:  # pragma: no cover
+    def __rand__(self, other: DaskExpr) -> Self:
         return self._from_call(
             lambda _input, other: _input.__rand__(other),
             "__rand__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __or__(self, other: DaskExpr) -> Self:
         return self._from_call(
@@ -369,13 +369,13 @@ class DaskExpr:
             returns_scalar=False,
         )
 
-    def __ror__(self, other: DaskExpr) -> Self:  # pragma: no cover
+    def __ror__(self, other: DaskExpr) -> Self:
         return self._from_call(
             lambda _input, other: _input.__ror__(other),
             "__ror__",
             other,
             returns_scalar=False,
-        )
+        ).alias("literal")
 
     def __invert__(self: Self) -> Self:
         return self._from_call(
diff --git a/narwhals/_expression_parsing.py b/narwhals/_expression_parsing.py
index 05056376..e8384165 100644
--- a/narwhals/_expression_parsing.py
+++ b/narwhals/_expression_parsing.py
@@ -235,7 +235,11 @@ def reuse_series_implementation(
         if expr._output_names is not None and (
             [s.name for s in out] != expr._output_names
         ):  # pragma: no cover
-            msg = "Safety assertion failed, please report a bug to https://github.com/narwhals-dev/narwhals/issues"
+            msg = (
+                f"Safety assertion failed, please report a bug to https://github.com/narwhals-dev/narwhals/issues\n"
+                f"Expression output names: {expr._output_names}\n"
+                f"Series names: {[s.name for s in out]}"
+            )
             raise AssertionError(msg)
         return out
 
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index 182ea980..80648a90 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -159,55 +159,64 @@ class PandasLikeExpr:
         return reuse_series_implementation(self, "__and__", other=other)
 
     def __rand__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__rand__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__and__(self)  # type: ignore[no-any-return]
 
     def __or__(self, other: PandasLikeExpr | bool | Any) -> Self:
         return reuse_series_implementation(self, "__or__", other=other)
 
     def __ror__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__ror__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__or__(self)  # type: ignore[no-any-return]
 
     def __add__(self, other: PandasLikeExpr | Any) -> Self:
         return reuse_series_implementation(self, "__add__", other=other)
 
     def __radd__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__radd__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__add__(self)  # type: ignore[no-any-return]
 
     def __sub__(self, other: PandasLikeExpr | Any) -> Self:
         return reuse_series_implementation(self, "__sub__", other=other)
 
     def __rsub__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__rsub__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__sub__(self)  # type: ignore[no-any-return]
 
     def __mul__(self, other: PandasLikeExpr | Any) -> Self:
         return reuse_series_implementation(self, "__mul__", other=other)
 
     def __rmul__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__rmul__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__mul__(self)  # type: ignore[no-any-return]
 
     def __truediv__(self, other: PandasLikeExpr | Any) -> Self:
         return reuse_series_implementation(self, "__truediv__", other=other)
 
     def __rtruediv__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__rtruediv__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__truediv__(self)  # type: ignore[no-any-return]
 
     def __floordiv__(self, other: PandasLikeExpr | Any) -> Self:
         return reuse_series_implementation(self, "__floordiv__", other=other)
 
     def __rfloordiv__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__rfloordiv__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__floordiv__(self)  # type: ignore[no-any-return]
 
     def __pow__(self, other: PandasLikeExpr | Any) -> Self:
         return reuse_series_implementation(self, "__pow__", other=other)
 
     def __rpow__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__rpow__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__pow__(self)  # type: ignore[no-any-return]
 
     def __mod__(self, other: PandasLikeExpr | Any) -> Self:
         return reuse_series_implementation(self, "__mod__", other=other)
 
     def __rmod__(self, other: Any) -> Self:
-        return reuse_series_implementation(self, "__rmod__", other=other)
+        other = self.__narwhals_namespace__().lit(other, dtype=None)
+        return other.__mod__(self)  # type: ignore[no-any-return]
 
     # Unary
 
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index b001f19d..60f5fa24 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -155,7 +155,7 @@ class PandasLikeNamespace:
             depth=0,
             function_name="lit",
             root_names=None,
-            output_names=[_lit_pandas_series.__name__],
+            output_names=["literal"],
             implementation=self._implementation,
             backend_version=self._backend_version,
             dtypes=self._dtypes,
@@ -501,8 +501,9 @@ class PandasWhen:
             )
         value_series = cast(PandasLikeSeries, value_series)
 
-        value_series_native = value_series._native_series
-        condition_native = validate_column_comparand(value_series_native.index, condition)
+        value_series_native, condition_native = validate_column_comparand(
+            value_series, condition
+        )
 
         if self._otherwise_value is None:
             return [
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 426e828b..ef8e784c 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -218,9 +218,9 @@ class PandasLikeSeries:
         if isinstance(values, self.__class__):
             # .copy() is necessary in some pre-2.2 versions of pandas to avoid
             # `values` also getting modified (!)
-            values = validate_column_comparand(self._native_series.index, values).copy()
+            _, values = validate_column_comparand(self, values)
             values = set_axis(
-                values,
+                values.copy(),
                 self._native_series.index[indices],
                 implementation=self._implementation,
                 backend_version=self._backend_version,
@@ -298,135 +298,135 @@ class PandasLikeSeries:
     def filter(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
         if not (isinstance(other, list) and all(isinstance(x, bool) for x in other)):
-            other = validate_column_comparand(self._native_series.index, other)
+            ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.loc[other].rename(ser.name, copy=False))
 
     def __eq__(self, other: object) -> PandasLikeSeries:  # type: ignore[override]
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__eq__(other).rename(ser.name, copy=False))
 
     def __ne__(self, other: object) -> PandasLikeSeries:  # type: ignore[override]
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__ne__(other).rename(ser.name, copy=False))
 
     def __ge__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__ge__(other).rename(ser.name, copy=False))
 
     def __gt__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__gt__(other).rename(ser.name, copy=False))
 
     def __le__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__le__(other).rename(ser.name, copy=False))
 
     def __lt__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__lt__(other).rename(ser.name, copy=False))
 
     def __and__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__and__(other).rename(ser.name, copy=False))
 
     def __rand__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
-        return self._from_native_series(ser.__rand__(other).rename(ser.name, copy=False))
+        ser, other = validate_column_comparand(self, other)
+        return self._from_native_series(ser.__and__(other).rename(ser.name, copy=False))
 
     def __or__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__or__(other).rename(ser.name, copy=False))
 
     def __ror__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
-        return self._from_native_series(ser.__ror__(other).rename(ser.name, copy=False))
+        ser, other = validate_column_comparand(self, other)
+        return self._from_native_series(ser.__or__(other).rename(ser.name, copy=False))
 
     def __add__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__add__(other).rename(ser.name, copy=False))
 
     def __radd__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__radd__(other).rename(ser.name, copy=False))
 
     def __sub__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__sub__(other).rename(ser.name, copy=False))
 
     def __rsub__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__rsub__(other).rename(ser.name, copy=False))
 
     def __mul__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__mul__(other).rename(ser.name, copy=False))
 
     def __rmul__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__rmul__(other).rename(ser.name, copy=False))
 
     def __truediv__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(
             ser.__truediv__(other).rename(ser.name, copy=False)
         )
 
     def __rtruediv__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(
             ser.__rtruediv__(other).rename(ser.name, copy=False)
         )
 
     def __floordiv__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(
             ser.__floordiv__(other).rename(ser.name, copy=False)
         )
 
     def __rfloordiv__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(
             ser.__rfloordiv__(other).rename(ser.name, copy=False)
         )
 
     def __pow__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__pow__(other).rename(ser.name, copy=False))
 
     def __rpow__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__rpow__(other).rename(ser.name, copy=False))
 
     def __mod__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__mod__(other).rename(ser.name, copy=False))
 
     def __rmod__(self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        other = validate_column_comparand(self._native_series.index, other)
+        ser, other = validate_column_comparand(self, other)
         return self._from_native_series(ser.__rmod__(other).rename(ser.name, copy=False))
 
     # Unary
@@ -738,9 +738,8 @@ class PandasLikeSeries:
         return self._native_series.quantile(q=quantile, interpolation=interpolation)
 
     def zip_with(self: Self, mask: Any, other: Any) -> PandasLikeSeries:
-        ser = self._native_series
-        mask = validate_column_comparand(ser.index, mask)
-        other = validate_column_comparand(ser.index, other)
+        ser, mask = validate_column_comparand(self, mask)
+        _, other = validate_column_comparand(self, other)
         res = ser.where(mask, other)
         return self._from_native_series(res)
 
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 44bfc34b..79be11aa 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -79,7 +79,7 @@ $"""
 PATTERN_PA_DURATION = re.compile(PA_DURATION_RGX, re.VERBOSE)
 
 
-def validate_column_comparand(index: Any, other: Any) -> Any:
+def validate_column_comparand(lhs: PandasLikeSeries, rhs: Any) -> tuple[pd.Series, Any]:
     """Validate RHS of binary operation.
 
     If the comparison isn't supported, return `NotImplemented` so that the
@@ -91,35 +91,56 @@ def validate_column_comparand(index: Any, other: Any) -> Any:
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
     from narwhals._pandas_like.series import PandasLikeSeries
 
-    if isinstance(other, list):
-        if len(other) > 1:
-            if hasattr(other[0], "__narwhals_expr__") or hasattr(
-                other[0], "__narwhals_series__"
+    # If `rhs` is the output of an expression evaluation, then it is
+    # a list of Series. So, we verify that that list is of length-1,
+    # and take the first (and only) element.
+    if isinstance(rhs, list):
+        if len(rhs) > 1:
+            if hasattr(rhs[0], "__narwhals_expr__") or hasattr(
+                rhs[0], "__narwhals_series__"
             ):
                 # e.g. `plx.all() + plx.all()`
                 msg = "Multi-output expressions (e.g. `nw.all()` or `nw.col('a', 'b')`) are not supported in this context"
                 raise ValueError(msg)
-            msg = (
-                f"Expected scalar value, Series, or Expr, got list of : {type(other[0])}"
-            )
+            msg = f"Expected scalar value, Series, or Expr, got list of : {type(rhs[0])}"
             raise ValueError(msg)
-        other = other[0]
-    if isinstance(other, PandasLikeDataFrame):
+        rhs = rhs[0]
+
+    lhs_index = lhs._native_series.index
+
+    if isinstance(rhs, PandasLikeDataFrame):
         return NotImplemented
-    if isinstance(other, PandasLikeSeries):
-        if other.len() == 1:
+
+    if isinstance(rhs, PandasLikeSeries):
+        rhs_index = rhs._native_series.index
+        if rhs.len() == 1:
             # broadcast
-            s = other._native_series
-            return s.__class__(s.iloc[0], index=index, dtype=s.dtype)
-        if other._native_series.index is not index:
-            return set_axis(
-                other._native_series,
-                index,
-                implementation=other._implementation,
-                backend_version=other._backend_version,
+            s = rhs._native_series
+            return (
+                lhs._native_series,
+                s.__class__(s.iloc[0], index=lhs_index, dtype=s.dtype),
             )
-        return other._native_series
-    return other
+        if lhs.len() == 1:
+            # broadcast
+            s = lhs._native_series
+            return (
+                s.__class__(s.iloc[0], index=rhs_index, dtype=s.dtype, name=s.name),
+                rhs._native_series,
+            )
+        if rhs._native_series.index is not lhs_index:
+            return (
+                lhs._native_series,
+                set_axis(
+                    rhs._native_series,
+                    lhs_index,
+                    implementation=rhs._implementation,
+                    backend_version=rhs._backend_version,
+                ),
+            )
+        return (lhs._native_series, rhs._native_series)
+
+    # `rhs` must be scalar, so just leave it as-is
+    return lhs._native_series, rhs
 
 
 def validate_dataframe_comparand(index: Any, other: Any) -> Any:
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index a0df340d..49291322 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -192,9 +192,11 @@ class PolarsSeries:
         )
 
     def __rpow__(self, other: PolarsSeries | Any) -> Self:
-        return self._from_native_series(
-            self._native_series.__rpow__(extract_native(other))
-        )
+        result = self._native_series.__rpow__(extract_native(other))
+        if self._backend_version < (16, 1):
+            # Explicitly set alias to work around https://github.com/pola-rs/polars/issues/20071
+            result = result.alias(self.name)
+        return self._from_native_series(result)
 
     def __invert__(self) -> Self:
         return self._from_native_series(self._native_series.__invert__())
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 245822bb..d5da607d 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -2971,9 +2971,10 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import polars as pl
             >>> import pyarrow as pa
             >>> import narwhals as nw
+            >>>
             >>> data = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]}
             >>> df_pd = pd.DataFrame(data)
-            >>> df_pl = pl.LazyFrame(data)
+            >>> lf_pl = pl.LazyFrame(data)
             >>> df_pa = pa.table(data)
 
             Calling `to_native` on a Narwhals DataFrame returns the native object:
@@ -2983,7 +2984,7 @@ class LazyFrame(BaseFrame[FrameT]):
             0    1  6.0   a
             1    2  7.0   b
             2    3  8.0   c
-            >>> nw.from_native(df_pl).to_native().collect()
+            >>> nw.from_native(lf_pl).to_native().collect()
             shape: (3, 3)
             ┌─────┬─────┬─────┐
             │ foo ┆ bar ┆ ham │
@@ -3009,7 +3010,7 @@ class LazyFrame(BaseFrame[FrameT]):
             >>>
             >>> data = {"a": [1, 2, 3], "ba": [4, 5, 6]}
             >>> df_pd = pd.DataFrame(data)
-            >>> df_pl = pl.LazyFrame(data)
+            >>> lf_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -3024,7 +3025,7 @@ class LazyFrame(BaseFrame[FrameT]):
             0  1
             1  2
             2  3
-            >>> agnostic_pipe(df_pl).collect()
+            >>> agnostic_pipe(lf_pl).collect()
             shape: (3, 1)
             ┌─────┐
             │ a   │
@@ -3057,7 +3058,7 @@ class LazyFrame(BaseFrame[FrameT]):
             >>>
             >>> data = {"a": [1.0, 2.0, None], "ba": [1.0, None, 2.0]}
             >>> df_pd = pd.DataFrame(data)
-            >>> df_pl = pl.LazyFrame(data)
+            >>> lf_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -3070,7 +3071,7 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> agnostic_drop_nulls(df_pd)
                  a   ba
             0  1.0  1.0
-            >>> agnostic_drop_nulls(df_pl).collect()
+            >>> agnostic_drop_nulls(lf_pl).collect()
             shape: (1, 2)
             ┌─────┬─────┐
             │ a   ┆ ba  │
@@ -3093,7 +3094,7 @@ class LazyFrame(BaseFrame[FrameT]):
             >>>
             >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
             >>> df_pd = pd.DataFrame(data)
-            >>> df_pl = pl.LazyFrame(data)
+            >>> lf_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -3108,7 +3109,7 @@ class LazyFrame(BaseFrame[FrameT]):
             0      0  1  4
             1      1  2  5
             2      2  3  6
-            >>> agnostic_with_row_index(df_pl).collect()
+            >>> agnostic_with_row_index(lf_pl).collect()
             shape: (3, 3)
             ┌───────┬─────┬─────┐
             │ index ┆ a   ┆ b   │
@@ -3216,6 +3217,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> df = {
             ...     "a": [1, 2, 3, 4],
             ...     "b": [0.5, 4, 10, 13],
@@ -3228,19 +3231,19 @@ class LazyFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function in which we pass an expression
             to add it as a new column:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns((nw.col("a") * 2).alias("2a"))
+            >>> def agnostic_with_columns(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.with_columns((nw.col("a") * 2).alias("2a")).to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_with_columns(df_pd)
                a     b      c  2a
             0  1   0.5   True   2
             1  2   4.0   True   4
             2  3  10.0  False   6
             3  4  13.0   True   8
-            >>> func(df_pl)
+            >>> agnostic_with_columns(df_pl)
             shape: (4, 4)
             ┌─────┬──────┬───────┬─────┐
             │ a   ┆ b    ┆ c     ┆ 2a  │
@@ -3252,7 +3255,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ 3   ┆ 10.0 ┆ false ┆ 6   │
             │ 4   ┆ 13.0 ┆ true  ┆ 8   │
             └─────┴──────┴───────┴─────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_with_columns(lf_pl).collect()
             shape: (4, 4)
             ┌─────┬──────┬───────┬─────┐
             │ a   ┆ b    ┆ c     ┆ 2a  │
@@ -3290,6 +3293,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> df = {
             ...     "foo": [1, 2, 3],
             ...     "bar": [6, 7, 8],
@@ -3302,18 +3307,18 @@ class LazyFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function in which we pass the name of a
             column to select that column.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select("foo")
+            >>> def agnostic_select(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.select("foo").to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_select(df_pd)
                foo
             0    1
             1    2
             2    3
-            >>> func(df_pl)
+            >>> agnostic_select(df_pl)
             shape: (3, 1)
             ┌─────┐
             │ foo │
@@ -3324,7 +3329,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ 2   │
             │ 3   │
             └─────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_select(lf_pl).collect()
             shape: (3, 1)
             ┌─────┐
             │ foo │
@@ -3338,15 +3343,16 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Multiple columns can be selected by passing a list of column names.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(["foo", "bar"])
-            >>> func(df_pd)
+            >>> def agnostic_select(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.select(["foo", "bar"]).to_native()
+            >>>
+            >>> agnostic_select(df_pd)
                foo  bar
             0    1    6
             1    2    7
             2    3    8
-            >>> func(df_pl)
+            >>> agnostic_select(df_pl)
             shape: (3, 2)
             ┌─────┬─────┐
             │ foo ┆ bar │
@@ -3357,7 +3363,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ 2   ┆ 7   │
             │ 3   ┆ 8   │
             └─────┴─────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_select(lf_pl).collect()
             shape: (3, 2)
             ┌─────┬─────┐
             │ foo ┆ bar │
@@ -3372,15 +3378,16 @@ class LazyFrame(BaseFrame[FrameT]):
             Multiple columns can also be selected using positional arguments instead of a
             list. Expressions are also accepted.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("foo"), nw.col("bar") + 1)
-            >>> func(df_pd)
+            >>> def agnostic_select(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.select(nw.col("foo"), nw.col("bar") + 1).to_native()
+            >>>
+            >>> agnostic_select(df_pd)
                foo  bar
             0    1    7
             1    2    8
             2    3    9
-            >>> func(df_pl)
+            >>> agnostic_select(df_pl)
             shape: (3, 2)
             ┌─────┬─────┐
             │ foo ┆ bar │
@@ -3391,7 +3398,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ 2   ┆ 8   │
             │ 3   ┆ 9   │
             └─────┴─────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_select(lf_pl).collect()
             shape: (3, 2)
             ┌─────┬─────┐
             │ foo ┆ bar │
@@ -3405,15 +3412,16 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Use keyword arguments to easily name your expression inputs.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(threshold=nw.col("foo") * 2)
-            >>> func(df_pd)
+            >>> def agnostic_select(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.select(threshold=nw.col("foo") * 2).to_native()
+            >>>
+            >>> agnostic_select(df_pd)
                threshold
             0          2
             1          4
             2          6
-            >>> func(df_pl)
+            >>> agnostic_select(df_pl)
             shape: (3, 1)
             ┌───────────┐
             │ threshold │
@@ -3424,7 +3432,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ 4         │
             │ 6         │
             └───────────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_select(lf_pl).collect()
             shape: (3, 1)
             ┌───────────┐
             │ threshold │
@@ -3450,24 +3458,26 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}
             >>> df_pd = pd.DataFrame(data)
             >>> lf_pl = pl.LazyFrame(data)
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.rename({"foo": "apple"})
+            >>> def agnostic_rename(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.rename({"foo": "apple"}).to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_rename(df_pd)
                apple  bar ham
             0      1    6   a
             1      2    7   b
             2      3    8   c
-            >>> func(lf_pl).collect()
+            >>> agnostic_rename(lf_pl).collect()
             shape: (3, 3)
             ┌───────┬─────┬─────┐
             │ apple ┆ bar ┆ ham │
@@ -3491,6 +3501,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {
             ...     "a": [1, 2, 3, 4, 5, 6],
             ...     "b": [7, 8, 9, 10, 11, 12],
@@ -3501,18 +3513,18 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function that gets the first 3 rows.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.head(3)
+            >>> def agnostic_head(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.head(3).to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_head(df_pd)
                a  b
             0  1  7
             1  2  8
             2  3  9
-            >>> func(df_pl)
+            >>> agnostic_head(df_pl)
             shape: (3, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -3523,7 +3535,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ 2   ┆ 8   │
             │ 3   ┆ 9   │
             └─────┴─────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_head(lf_pl).collect()
             shape: (3, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -3547,6 +3559,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {
             ...     "a": [1, 2, 3, 4, 5, 6],
             ...     "b": [7, 8, 9, 10, 11, 12],
@@ -3557,18 +3571,18 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function that gets the last 3 rows.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.tail(3)
+            >>> def agnostic_tail(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.tail(3).to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_tail(df_pd)
                a   b
             3  4  10
             4  5  11
             5  6  12
-            >>> func(df_pl)
+            >>> agnostic_tail(df_pl)
             shape: (3, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -3579,7 +3593,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ 5   ┆ 11  │
             │ 6   ┆ 12  │
             └─────┴─────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_tail(lf_pl).collect()
             shape: (3, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -3610,24 +3624,26 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]}
             >>> df_pd = pd.DataFrame(data)
             >>> lf_pl = pl.LazyFrame(data)
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.drop("ham")
+            >>> def agnostic_drop(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.drop("ham").to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_drop(df_pd)
                foo  bar
             0    1  6.0
             1    2  7.0
             2    3  8.0
-            >>> func(lf_pl).collect()
+            >>> agnostic_drop(lf_pl).collect()
             shape: (3, 2)
             ┌─────┬─────┐
             │ foo ┆ bar │
@@ -3641,16 +3657,16 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Use positional arguments to drop multiple columns.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.drop("foo", "ham")
+            >>> def agnostic_drop(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.drop("foo", "ham").to_native()
 
-            >>> func(df_pd)
+            >>> agnostic_drop(df_pd)
                bar
             0  6.0
             1  7.0
             2  8.0
-            >>> func(lf_pl).collect()
+            >>> agnostic_drop(lf_pl).collect()
             shape: (3, 1)
             ┌─────┐
             │ bar │
@@ -3695,6 +3711,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {
             ...     "foo": [1, 2, 3, 1],
             ...     "bar": ["a", "a", "a", "a"],
@@ -3705,16 +3723,16 @@ class LazyFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.unique(["bar", "ham"])
+            >>> def agnostic_unique(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.unique(["bar", "ham"]).to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_unique(df_pd)
                foo bar ham
             0    1   a   b
-            >>> func(lf_pl).collect()
+            >>> agnostic_unique(lf_pl).collect()
             shape: (1, 3)
             ┌─────┬─────┬─────┐
             │ foo ┆ bar ┆ ham │
@@ -3744,7 +3762,7 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
-            >>> from narwhals.typing import IntoFrame
+            >>> from narwhals.typing import IntoFrameT
             >>>
             >>> data = {
             ...     "foo": [1, 2, 3],
@@ -3758,7 +3776,7 @@ class LazyFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function in which we filter on
             one condition.
 
-            >>> def agnostic_filter(df_native: IntoFrame) -> IntoFrame:
+            >>> def agnostic_filter(df_native: IntoFrameT) -> IntoFrameT:
             ...     df = nw.from_native(df_native)
             ...     return df.filter(nw.col("foo") > 1).to_native()
 
@@ -3791,10 +3809,10 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Filter on multiple conditions:
 
-
-            >>> def agnostic_filter(df_native: IntoFrame) -> IntoFrame:
+            >>> def agnostic_filter(df_native: IntoFrameT) -> IntoFrameT:
             ...     df = nw.from_native(df_native)
             ...     return df.filter((nw.col("foo") < 3) & (nw.col("ham") == "a")).to_native()
+            >>>
             >>> agnostic_filter(df_pd)
                foo  bar ham
             0    1    6   a
@@ -3819,13 +3837,13 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Provide multiple filters using `*args` syntax:
 
-            >>> def agnostic_filter(df_native: IntoFrame) -> IntoFrame:
+            >>> def agnostic_filter(df_native: IntoFrameT) -> IntoFrameT:
             ...     df = nw.from_native(df_native)
-            ...     dframe = df.filter(
+            ...     return df.filter(
             ...         nw.col("foo") == 1,
             ...         nw.col("ham") == "a",
-            ...     )
-            ...     return dframe.to_native()
+            ...     ).to_native()
+            >>>
             >>> agnostic_filter(df_pd)
                foo  bar ham
             0    1    6   a
@@ -3850,12 +3868,12 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Filter on an OR condition:
 
-            >>> def agnostic_filter(df_native: IntoFrame) -> IntoFrame:
+            >>> def agnostic_filter(df_native: IntoFrameT) -> IntoFrameT:
             ...     df = nw.from_native(df_native)
-            ...     dframe = df.filter(
+            ...     return df.filter(
             ...         (nw.col("foo") == 1) | (nw.col("ham") == "c")
             ...     ).to_native()
-            ...     return dframe
+            >>>
             >>> agnostic_filter(df_pd)
                foo  bar ham
             0    1    6   a
@@ -3883,9 +3901,10 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Provide multiple filters using `**kwargs` syntax:
 
-            >>> def agnostic_filter(df_native: IntoFrame) -> IntoFrame:
+            >>> def agnostic_filter(df_native: IntoFrameT) -> IntoFrameT:
             ...     df = nw.from_native(df_native)
             ...     return df.filter(foo=2, ham="b").to_native()
+            >>>
             >>> agnostic_filter(df_pd)
                foo  bar ham
             1    2    7   b
@@ -3929,6 +3948,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> df = {
             ...     "a": ["a", "b", "a", "b", "c"],
             ...     "b": [1, 2, 1, 3, 3],
@@ -3941,18 +3962,18 @@ class LazyFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function in which we group by one column
             and call `agg` to compute the grouped sum of another column.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.group_by("a").agg(nw.col("b").sum()).sort("a")
+            >>> def agnostic_group_by_agg(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.group_by("a").agg(nw.col("b").sum()).sort("a").to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_group_by_agg(df_pd)
                a  b
             0  a  2
             1  b  5
             2  c  3
-            >>> func(df_pl)
+            >>> agnostic_group_by_agg(df_pl)
             shape: (3, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -3963,7 +3984,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ b   ┆ 5   │
             │ c   ┆ 3   │
             └─────┴─────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_group_by_agg(lf_pl).collect()
             shape: (3, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -3977,16 +3998,19 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Group by multiple columns by passing a list of column names.
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.group_by(["a", "b"]).agg(nw.max("c")).sort(["a", "b"])
-            >>> func(df_pd)
+            >>> def agnostic_group_by_agg(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return (
+            ...         df.group_by(["a", "b"]).agg(nw.max("c")).sort(["a", "b"]).to_native()
+            ...     )
+            >>>
+            >>> agnostic_group_by_agg(df_pd)
                a  b  c
             0  a  1  5
             1  b  2  4
             2  b  3  2
             3  c  3  1
-            >>> func(df_pl)
+            >>> agnostic_group_by_agg(df_pl)
             shape: (4, 3)
             ┌─────┬─────┬─────┐
             │ a   ┆ b   ┆ c   │
@@ -3998,7 +4022,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ b   ┆ 3   ┆ 2   │
             │ c   ┆ 3   ┆ 1   │
             └─────┴─────┴─────┘
-            >>> func(lf_pl).collect()
+            >>> agnostic_group_by_agg(lf_pl).collect()
             shape: (4, 3)
             ┌─────┬─────┬─────┐
             │ a   ┆ b   ┆ c   │
@@ -4041,29 +4065,31 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {
             ...     "a": [1, 2, None],
             ...     "b": [6.0, 5.0, 4.0],
             ...     "c": ["a", "c", "b"],
             ... }
             >>> df_pd = pd.DataFrame(data)
-            >>> df_lf = pl.LazyFrame(data)
+            >>> lf_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function in which we sort by multiple
             columns in different orders
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.sort("c", "a", descending=[False, True])
+            >>> def agnostic_sort(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.sort("c", "a", descending=[False, True]).to_native()
 
             We can then pass either pandas or Polars to `func`:
 
-            >>> func(df_pd)
+            >>> agnostic_sort(df_pd)
                  a    b  c
             0  1.0  6.0  a
             2  NaN  4.0  b
             1  2.0  5.0  c
-            >>> func(df_lf).collect()
+            >>> agnostic_sort(lf_pl).collect()
             shape: (3, 3)
             ┌──────┬─────┬─────┐
             │ a    ┆ b   ┆ c   │
@@ -4111,6 +4137,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {
             ...     "foo": [1, 2, 3],
             ...     "bar": [6.0, 7.0, 8.0],
@@ -4124,23 +4152,27 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> df_pd = pd.DataFrame(data)
             >>> other_pd = pd.DataFrame(data_other)
 
-            >>> df_pl = pl.LazyFrame(data)
+            >>> lf_pl = pl.LazyFrame(data)
             >>> other_pl = pl.LazyFrame(data_other)
 
             Let's define a dataframe-agnostic function in which we join over "ham" column:
 
-            >>> @nw.narwhalify
-            ... def join_on_ham(df, other_any):
-            ...     return df.join(other_any, left_on="ham", right_on="ham")
+            >>> def agnostic_join_on_ham(
+            ...     df_native: IntoFrameT,
+            ...     other_native: IntoFrameT,
+            ... ) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     other = nw.from_native(other_native)
+            ...     return df.join(other, left_on="ham", right_on="ham").to_native()
 
             We can now pass either pandas or Polars to the function:
 
-            >>> join_on_ham(df_pd, other_pd)
+            >>> agnostic_join_on_ham(df_pd, other_pd)
                foo  bar ham apple
             0    1  6.0   a     x
             1    2  7.0   b     y
 
-            >>> join_on_ham(df_pl, other_pl).collect()
+            >>> agnostic_join_on_ham(lf_pl, other_pl).collect()
             shape: (2, 4)
             ┌─────┬─────┬─────┬───────┐
             │ foo ┆ bar ┆ ham ┆ apple │
@@ -4202,6 +4234,9 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from typing import Literal
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data_gdp = {
             ...     "datetime": [
             ...         datetime(2016, 1, 1),
@@ -4227,19 +4262,26 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function in which we join over "datetime" column:
 
-            >>> @nw.narwhalify
-            ... def join_asof_datetime(df, other_any, strategy):
-            ...     return df.join_asof(other_any, on="datetime", strategy=strategy)
+            >>> def agnostic_join_asof_datetime(
+            ...     df_native: IntoFrameT,
+            ...     other_native: IntoFrameT,
+            ...     strategy: Literal["backward", "forward", "nearest"],
+            ... ) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     other = nw.from_native(other_native)
+            ...     return df.join_asof(other, on="datetime", strategy=strategy).to_native()
 
             We can now pass either pandas or Polars to the function:
 
-            >>> join_asof_datetime(population_pd, gdp_pd, strategy="backward")
+            >>> agnostic_join_asof_datetime(population_pd, gdp_pd, strategy="backward")
                 datetime  population   gdp
             0 2016-03-01       82.19  4164
             1 2018-08-01       82.66  4566
             2 2019-01-01       83.12  4696
 
-            >>> join_asof_datetime(population_pl, gdp_pl, strategy="backward").collect()
+            >>> agnostic_join_asof_datetime(
+            ...     population_pl, gdp_pl, strategy="backward"
+            ... ).collect()
             shape: (3, 3)
             ┌─────────────────────┬────────────┬──────┐
             │ datetime            ┆ population ┆ gdp  │
@@ -4257,6 +4299,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data_quotes = {
             ...     "datetime": [
             ...         datetime(2016, 5, 25, 13, 30, 0, 23),
@@ -4300,13 +4344,17 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function in which we join over "datetime" and by "ticker" columns:
 
-            >>> @nw.narwhalify
-            ... def join_asof_datetime_by_ticker(df, other_any):
-            ...     return df.join_asof(other_any, on="datetime", by="ticker")
+            >>> def agnostic_join_asof_datetime_by_ticker(
+            ...     df_native: IntoFrameT,
+            ...     other_native: IntoFrameT,
+            ... ) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     other = nw.from_native(other_native)
+            ...     return df.join_asof(other, on="datetime", by="ticker").to_native()
 
             We can now pass either pandas or Polars to the function:
 
-            >>> join_asof_datetime_by_ticker(trades_pd, quotes_pd)
+            >>> agnostic_join_asof_datetime_by_ticker(trades_pd, quotes_pd)
                                 datetime ticker   price  quantity     bid     ask
             0 2016-05-25 13:30:00.000023   MSFT   51.95        75   51.95   51.96
             1 2016-05-25 13:30:00.000038   MSFT   51.95       155   51.97   51.98
@@ -4314,7 +4362,7 @@ class LazyFrame(BaseFrame[FrameT]):
             3 2016-05-25 13:30:00.000048   GOOG  720.92       100  720.50  720.93
             4 2016-05-25 13:30:00.000048   AAPL   98.00       100     NaN     NaN
 
-            >>> join_asof_datetime_by_ticker(trades_pl, quotes_pl).collect()
+            >>> agnostic_join_asof_datetime_by_ticker(trades_pl, quotes_pl).collect()
             shape: (5, 6)
             ┌────────────────────────────┬────────┬────────┬──────────┬───────┬────────┐
             │ datetime                   ┆ ticker ┆ price  ┆ quantity ┆ bid   ┆ ask    │
@@ -4346,22 +4394,24 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"a": [1, 2], "b": [3, 4]}
             >>> df_pd = pd.DataFrame(data)
-            >>> df_pl = pl.LazyFrame(data)
+            >>> lf_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function in which we copy the DataFrame:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.clone()
+            >>> def agnostic_clone(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.clone().to_native()
 
-            >>> func(df_pd)
+            >>> agnostic_clone(df_pd)
                a  b
             0  1  3
             1  2  4
 
-            >>> func(df_pl).collect()
+            >>> agnostic_clone(lf_pl).collect()
             shape: (2, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -4385,24 +4435,26 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> df = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]}
             >>> df_pd = pd.DataFrame(df)
-            >>> df_pl = pl.LazyFrame(df)
+            >>> lf_pl = pl.LazyFrame(df)
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.lazy()
+            >>> def agnostic_lazy(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.lazy().to_native()
 
             Note that then, pandas dataframe stay eager, and the Polars LazyFrame stays lazy:
 
-            >>> func(df_pd)
+            >>> agnostic_lazy(df_pd)
                foo  bar ham
             0    1  6.0   a
             1    2  7.0   b
             2    3  8.0   c
-            >>> func(df_pl)
+            >>> agnostic_lazy(lf_pl)
             <LazyFrame ...>
         """
         return self
@@ -4418,6 +4470,8 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {"a": [1, 2, 3, 4], "b": [5, 6, 7, 8]}
             >>> df_pd = pd.DataFrame(data)
             >>> lf_pl = pl.LazyFrame(data)
@@ -4425,16 +4479,16 @@ class LazyFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function in which gather every 2 rows,
             starting from a offset of 1:
 
-            >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.gather_every(n=2, offset=1)
+            >>> def agnostic_gather_every(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
+            ...     return df.gather_every(n=2, offset=1).to_native()
 
-            >>> func(df_pd)
+            >>> agnostic_gather_every(df_pd)
                a  b
             1  2  6
             3  4  8
 
-            >>> func(lf_pl).collect()
+            >>> agnostic_gather_every(lf_pl).collect()
             shape: (2, 2)
             ┌─────┬─────┐
             │ a   ┆ b   │
@@ -4479,21 +4533,24 @@ class LazyFrame(BaseFrame[FrameT]):
         Examples:
             >>> import narwhals as nw
             >>> import polars as pl
+            >>> from narwhals.typing import IntoFrameT
+            >>>
             >>> data = {
             ...     "a": ["x", "y", "z"],
             ...     "b": [1, 3, 5],
             ...     "c": [2, 4, 6],
             ... }
+            >>> lf_pl = pl.LazyFrame(data)
 
             We define a library agnostic function:
 
-            >>> @nw.narwhalify
-            ... def func(lf):
+            >>> def agnostic_unpivot(df_native: IntoFrameT) -> IntoFrameT:
+            ...     df = nw.from_native(df_native)
             ...     return (
-            ...         lf.unpivot(on=["b", "c"], index="a").sort(["variable", "a"]).collect()
-            ...     )
+            ...         df.unpivot(on=["b", "c"], index="a").sort(["variable", "a"])
+            ...     ).to_native()
 
-            >>> func(pl.LazyFrame(data))
+            >>> agnostic_unpivot(lf_pl).collect()
             shape: (6, 3)
             ┌─────┬──────────┬───────┐
             │ a   ┆ variable ┆ value │
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 2862f938..0e6ea156 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -3988,7 +3988,9 @@ class ExprStringNamespace(Generic[ExprT]):
             │ zukkyun   ┆ kkyun       │
             └───────────┴─────────────┘
         """
-        return self._expr.__class__(lambda plx: self._expr._call(plx).str.slice(-n))
+        return self._expr.__class__(
+            lambda plx: self._expr._call(plx).str.slice(offset=-n, length=None)
+        )
 
     def to_datetime(self: Self, format: str | None = None) -> ExprT:  # noqa: A002
         """Convert to Datetime dtype.
diff --git a/narwhals/series.py b/narwhals/series.py
index cd9055cb..6c011610 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -2037,11 +2037,21 @@ class Series(Generic[IntoSeriesT]):
             self._compliant_series.__and__(self._extract_native(other))
         )
 
+    def __rand__(self, other: Any) -> Self:
+        return self._from_compliant_series(
+            self._compliant_series.__rand__(self._extract_native(other))
+        )
+
     def __or__(self, other: Any) -> Self:
         return self._from_compliant_series(
             self._compliant_series.__or__(self._extract_native(other))
         )
 
+    def __ror__(self, other: Any) -> Self:
+        return self._from_compliant_series(
+            self._compliant_series.__ror__(self._extract_native(other))
+        )
+
     # unary
     def __invert__(self) -> Self:
         return self._from_compliant_series(self._compliant_series.__invert__())
@@ -3805,7 +3815,7 @@ class SeriesStringNamespace(Generic[SeriesT]):
             ]
         """
         return self._narwhals_series._from_compliant_series(
-            self._narwhals_series._compliant_series.str.slice(0, n)
+            self._narwhals_series._compliant_series.str.slice(offset=0, length=n)
         )
 
     def tail(self: Self, n: int = 5) -> SeriesT:
@@ -3853,7 +3863,7 @@ class SeriesStringNamespace(Generic[SeriesT]):
             ]
         """
         return self._narwhals_series._from_compliant_series(
-            self._narwhals_series._compliant_series.str.slice(-n)
+            self._narwhals_series._compliant_series.str.slice(offset=-n, length=None)
         )
 
     def to_uppercase(self) -> SeriesT:
diff --git a/pyproject.toml b/pyproject.toml
index 3e43940c..79ae858d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -148,8 +148,9 @@ exclude_also = [
   "if (:?self._)?implementation is Implementation.MODIN",
   "if (:?self._)?implementation is Implementation.CUDF",
   'request.applymarker\(pytest.mark.xfail\)',
-  'if self._backend_version < ',
-  'if "cudf" in str\(constructor',
+  'if \w+._backend_version < ',
+  'if backend_version <',
+  'if "cudf" in str\(constructor'
 ]
 
 [tool.mypy]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 0df99e50..fb5afb25 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -5,6 +5,7 @@ pandas
 polars
 pre-commit
 pyarrow
+pyarrow-stubs
 pytest
 pytest-cov
 pytest-randomly
diff --git a/utils/bump_version.py b/utils/bump_version.py
index 4230e689..59d1aa7f 100644
--- a/utils/bump_version.py
+++ b/utils/bump_version.py
@@ -13,6 +13,15 @@ if out.returncode != 0:
     sys.exit(1)
 subprocess.run(["git", "reset", "--hard", "upstream/main"])
 
+if (
+    subprocess.run(
+        ["git", "branch", "--show-current"], text=True, capture_output=True
+    ).stdout.strip()
+    != "bump-version"
+):
+    msg = "`bump_version.py` should be run from `bump-version` branch"
+    raise RuntimeError(msg)
+
 # Delete local tags, if present
 try:
     # Get the list of all tags


Test patch:
diff --git a/tests/expr_and_series/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py
index 95172bd2..12f931ba 100644
--- a/tests/expr_and_series/arithmetic_test.py
+++ b/tests/expr_and_series/arithmetic_test.py
@@ -74,8 +74,8 @@ def test_right_arithmetic_expr(
 
     data = {"a": [1, 2, 3]}
     df = nw.from_native(constructor(data))
-    result = df.select(a=getattr(nw.col("a"), attr)(rhs))
-    assert_equal_data(result, {"a": expected})
+    result = df.select(getattr(nw.col("a"), attr)(rhs))
+    assert_equal_data(result, {"literal": expected})
 
 
 @pytest.mark.parametrize(
@@ -135,8 +135,9 @@ def test_right_arithmetic_series(
 
     data = {"a": [1, 2, 3]}
     df = nw.from_native(constructor_eager(data), eager_only=True)
-    result = df.select(a=getattr(df["a"], attr)(rhs))
-    assert_equal_data(result, {"a": expected})
+    result_series = getattr(df["a"], attr)(rhs)
+    assert result_series.name == "a"
+    assert_equal_data({"a": result_series}, {"a": expected})
 
 
 def test_truediv_same_dims(
@@ -218,3 +219,65 @@ def test_mod(left: int, right: int) -> None:
         nw.col("a") % right
     )
     assert_equal_data(result, expected)
+
+
+@pytest.mark.parametrize(
+    ("attr", "lhs", "expected"),
+    [
+        ("__add__", nw.lit(1), [2, 3, 5]),
+        ("__sub__", nw.lit(1), [0, -1, -3]),
+        ("__mul__", nw.lit(2), [2, 4, 8]),
+        ("__truediv__", nw.lit(2.0), [2, 1, 0.5]),
+        ("__truediv__", nw.lit(1), [1, 0.5, 0.25]),
+        ("__floordiv__", nw.lit(2), [2, 1, 0]),
+        ("__mod__", nw.lit(3), [0, 1, 3]),
+        ("__pow__", nw.lit(2), [2, 4, 16]),
+    ],
+)
+def test_arithmetic_expr_left_literal(
+    attr: str,
+    lhs: Any,
+    expected: list[Any],
+    constructor: Constructor,
+    request: pytest.FixtureRequest,
+) -> None:
+    if attr == "__mod__" and any(
+        x in str(constructor) for x in ["pandas_pyarrow", "modin"]
+    ):
+        request.applymarker(pytest.mark.xfail)
+
+    data = {"a": [1.0, 2, 4]}
+    df = nw.from_native(constructor(data))
+    result = df.select(getattr(lhs, attr)(nw.col("a")))
+    assert_equal_data(result, {"literal": expected})
+
+
+@pytest.mark.parametrize(
+    ("attr", "lhs", "expected"),
+    [
+        ("__add__", nw.lit(1), [2, 3, 5]),
+        ("__sub__", nw.lit(1), [0, -1, -3]),
+        ("__mul__", nw.lit(2), [2, 4, 8]),
+        ("__truediv__", nw.lit(2.0), [2, 1, 0.5]),
+        ("__truediv__", nw.lit(1), [1, 0.5, 0.25]),
+        ("__floordiv__", nw.lit(2), [2, 1, 0]),
+        ("__mod__", nw.lit(3), [0, 1, 3]),
+        ("__pow__", nw.lit(2), [2, 4, 16]),
+    ],
+)
+def test_arithmetic_series_left_literal(
+    attr: str,
+    lhs: Any,
+    expected: list[Any],
+    constructor_eager: ConstructorEager,
+    request: pytest.FixtureRequest,
+) -> None:
+    if attr == "__mod__" and any(
+        x in str(constructor_eager) for x in ["pandas_pyarrow", "modin"]
+    ):
+        request.applymarker(pytest.mark.xfail)
+
+    data = {"a": [1.0, 2, 4]}
+    df = nw.from_native(constructor_eager(data))
+    result = df.select(getattr(lhs, attr)(nw.col("a")))
+    assert_equal_data(result, {"literal": expected})
diff --git a/tests/expr_and_series/operators_test.py b/tests/expr_and_series/operators_test.py
index 5506e6a8..ff01747a 100644
--- a/tests/expr_and_series/operators_test.py
+++ b/tests/expr_and_series/operators_test.py
@@ -65,6 +65,25 @@ def test_logic_operators_expr(
     assert_equal_data(result, {"a": expected})
 
 
+@pytest.mark.parametrize(
+    ("operator", "expected"),
+    [
+        ("__and__", [False, False, False, False]),
+        ("__rand__", [False, False, False, False]),
+        ("__or__", [True, True, False, False]),
+        ("__ror__", [True, True, False, False]),
+    ],
+)
+def test_logic_operators_expr_scalar(
+    constructor: Constructor, operator: str, expected: list[bool]
+) -> None:
+    data = {"a": [True, True, False, False]}
+    df = nw.from_native(constructor(data))
+
+    result = df.select(a=getattr(nw.col("a"), operator)(False))  # noqa: FBT003
+    assert_equal_data(result, {"a": expected})
+
+
 @pytest.mark.parametrize(
     ("operator", "expected"),
     [
@@ -110,7 +129,9 @@ def test_comparand_operators_series(
     ("operator", "expected"),
     [
         ("__and__", [True, False, False, False]),
+        ("__rand__", [True, False, False, False]),
         ("__or__", [True, True, True, False]),
+        ("__ror__", [True, True, True, False]),
     ],
 )
 def test_logic_operators_series(
diff --git a/tests/frame/lit_test.py b/tests/frame/lit_test.py
index 2b0332a5..f51bd5c7 100644
--- a/tests/frame/lit_test.py
+++ b/tests/frame/lit_test.py
@@ -62,3 +62,33 @@ def test_lit_out_name(constructor: Constructor) -> None:
         "literal": [2, 2, 2],
     }
     assert_equal_data(result, expected)
+
+
+@pytest.mark.parametrize(
+    ("col_name", "expr", "expected_result"),
+    [
+        ("left_lit", nw.lit(1) + nw.col("a"), [2, 4, 3]),
+        ("right_lit", nw.col("a") + nw.lit(1), [2, 4, 3]),
+        ("left_lit_with_agg", nw.lit(1) + nw.col("a").mean(), [3]),
+        ("right_lit_with_agg", nw.col("a").mean() - nw.lit(1), [1]),
+        ("left_scalar", 1 + nw.col("a"), [2, 4, 3]),
+        ("right_scalar", nw.col("a") + 1, [2, 4, 3]),
+        ("left_scalar_with_agg", 1 + nw.col("a").mean(), [3]),
+        ("right_scalar_with_agg", nw.col("a").mean() - 1, [1]),
+    ],
+)
+def test_lit_operation(
+    constructor: Constructor,
+    col_name: str,
+    expr: nw.Expr,
+    expected_result: list[int],
+    request: pytest.FixtureRequest,
+) -> None:
+    if "dask_lazy_p2" in str(constructor) and "lit_with_agg" in col_name:
+        request.applymarker(pytest.mark.xfail)
+    data = {"a": [1, 3, 2]}
+    df_raw = constructor(data)
+    df = nw.from_native(df_raw).lazy()
+    result = df.select(expr.alias(col_name))
+    expected = {col_name: expected_result}
+    assert_equal_data(result, expected)
diff --git a/tests/utils.py b/tests/utils.py
index 90143959..f9b493ad 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -62,7 +62,7 @@ def assert_equal_data(result: Any, expected: dict[str, Any]) -> None:
         result = result.collect()
     if hasattr(result, "columns"):
         for key in result.columns:
-            assert key in expected
+            assert key in expected, (key, expected)
     result = {key: _to_comparable_list(result[key]) for key in expected}
     for key in expected:
         result_key = result[key]