Repository: narwhals-dev/narwhals
Pull request #1218 (2024-10-18 12:19:04)

docs: Pyarrow example docstrings for `Expr` and stable v1 API
## What type of PR is this? (check all applicable)

- [x] 📝 Documentation

## Related issues 

- Related issue #1000 

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added 
- [x] Documented the changes

## If you have comments or can explain your changes, please do so below.

PyArrow docstring examples for `narwhals/expr.py` and `narwhals/stable/v1/__init__.py`. 

I know #1000 mentions to do 4 functions in 1 PR, but was in a good flow so did the full `expr.py`. 😊  I think it's still within reason for review.




Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c7d7c44a..b8f333f1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -109,6 +109,10 @@ nox
 
 Notice that nox will also require to have all the python versions that are defined in the `noxfile.py` installed in your system.
 
+#### Testing cuDF
+
+We can't currently test in CI against cuDF, but you can test it manually in Kaggle using GPUs. Please follow this [Kaggle notebook](https://www.kaggle.com/code/marcogorelli/testing-cudf-in-narwhals) to run the tests.
+
 ### 7. Building docs
 
 To build the docs, run `mkdocs serve`, and then open the link provided in a browser.
diff --git a/docs/api-reference/narwhals.md b/docs/api-reference/narwhals.md
index b8ec2d79..c4b04a2f 100644
--- a/docs/api-reference/narwhals.md
+++ b/docs/api-reference/narwhals.md
@@ -39,4 +39,5 @@ Here are the top-level functions available in Narwhals.
         - when
         - show_versions
         - to_native
+        - to_py_scalar
       show_source: false
diff --git a/docs/installation.md b/docs/installation.md
index 66812671..1695a7ee 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -85,4 +85,4 @@ If you run `python t.py` then your output should look like the above. This is th
 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 and Polars. Please see the following to find the [supported libraries](extending.md).
+Note: these examples are only using pandas, Polars and PyArrow. Please see the following to find the [supported libraries](extending.md).
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index aeba3ef5..8dd76d08 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -58,6 +58,7 @@ from narwhals.translate import from_native
 from narwhals.translate import get_native_namespace
 from narwhals.translate import narwhalify
 from narwhals.translate import to_native
+from narwhals.translate import to_py_scalar
 from narwhals.utils import is_ordered_categorical
 from narwhals.utils import maybe_align_index
 from narwhals.utils import maybe_convert_dtypes
@@ -84,6 +85,7 @@ __all__ = [
     "maybe_reset_index",
     "maybe_set_index",
     "get_native_namespace",
+    "to_py_scalar",
     "all",
     "all_horizontal",
     "any_horizontal",
diff --git a/narwhals/_expression_parsing.py b/narwhals/_expression_parsing.py
index d281cc94..89d020b0 100644
--- a/narwhals/_expression_parsing.py
+++ b/narwhals/_expression_parsing.py
@@ -219,19 +219,21 @@ def reuse_series_implementation(
     plx = expr.__narwhals_namespace__()
 
     def func(df: CompliantDataFrame) -> list[CompliantSeries]:
-        out: list[CompliantSeries] = []
-        for column in expr._call(df):  # type: ignore[arg-type]
-            _out = getattr(column, attr)(
-                *[maybe_evaluate_expr(df, arg) for arg in args],
-                **{
-                    arg_name: maybe_evaluate_expr(df, arg_value)
-                    for arg_name, arg_value in kwargs.items()
-                },
+        _args = [maybe_evaluate_expr(df, arg) for arg in args]
+        _kwargs = {
+            arg_name: maybe_evaluate_expr(df, arg_value)
+            for arg_name, arg_value in kwargs.items()
+        }
+
+        out: list[CompliantSeries] = [
+            plx._create_series_from_scalar(
+                getattr(column, attr)(*_args, **_kwargs),
+                column,  # type: ignore[arg-type]
             )
-            if returns_scalar:
-                out.append(plx._create_series_from_scalar(_out, column))  # type: ignore[arg-type]
-            else:
-                out.append(_out)
+            if returns_scalar
+            else getattr(column, attr)(*_args, **_kwargs)
+            for column in expr._call(df)  # type: ignore[arg-type]
+        ]
         if expr._output_names is not None and (
             [s.name for s in out] != expr._output_names
         ):  # pragma: no cover
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
index 0ef2f879..ff80148c 100644
--- a/narwhals/_polars/dataframe.py
+++ b/narwhals/_polars/dataframe.py
@@ -68,6 +68,12 @@ class PolarsDataFrame:
     def __getattr__(self, attr: str) -> Any:
         if attr == "collect":  # pragma: no cover
             raise AttributeError
+        if attr == "schema":
+            schema = self._native_frame.schema
+            return {
+                name: native_to_narwhals_dtype(dtype, self._dtypes)
+                for name, dtype in schema.items()
+            }
 
         def func(*args: Any, **kwargs: Any) -> Any:
             args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
@@ -85,14 +91,6 @@ class PolarsDataFrame:
             return self._native_frame.__array__(dtype)
         return self._native_frame.__array__(dtype)
 
-    @property
-    def schema(self) -> dict[str, Any]:
-        schema = self._native_frame.schema
-        return {
-            name: native_to_narwhals_dtype(dtype, self._dtypes)
-            for name, dtype in schema.items()
-        }
-
     def collect_schema(self) -> dict[str, Any]:
         if self._backend_version < (1,):  # pragma: no cover
             schema = self._native_frame.schema
diff --git a/narwhals/_polars/utils.py b/narwhals/_polars/utils.py
index fe63f515..ac6ffb2b 100644
--- a/narwhals/_polars/utils.py
+++ b/narwhals/_polars/utils.py
@@ -84,7 +84,7 @@ def native_to_narwhals_dtype(dtype: Any, dtypes: DTypes) -> DType:
     if dtype == pl.List:
         return dtypes.List(native_to_narwhals_dtype(dtype.inner, dtypes))
     if dtype == pl.Array:
-        if parse_version(pl.__version__) < (1, 0):  # pragma: no cover
+        if parse_version(pl.__version__) < (0, 20, 30):  # pragma: no cover
             return dtypes.Array(
                 native_to_narwhals_dtype(dtype.inner, dtypes), dtype.width
             )
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index ac814afc..4645cfbb 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -582,9 +582,9 @@ class DataFrame(BaseFrame[DataFrameT]):
 
             We can then pass either pandas, Polars or PyArrow to `func`:
 
-            >>> func(df_pd)
-            >>> func(df_pl)
-            >>> func(df_pa)
+            >>> func(df_pd)  # doctest:+SKIP
+            >>> func(df_pl)  # doctest:+SKIP
+            >>> func(df_pa)  # doctest:+SKIP
         """
         self._compliant_frame.write_parquet(file)
 
diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py
index 144c57c8..1f9ae19f 100644
--- a/narwhals/dependencies.py
+++ b/narwhals/dependencies.py
@@ -46,6 +46,11 @@ def get_cudf() -> Any:
     return sys.modules.get("cudf", None)
 
 
+def get_cupy() -> Any:
+    """Get cupy module (if already imported - else return None)."""
+    return sys.modules.get("cupy", None)
+
+
 def get_pyarrow() -> Any:  # pragma: no cover
     """Get pyarrow module (if already imported - else return None)."""
     return sys.modules.get("pyarrow", None)
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 97c2e2d3..908db905 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -3,9 +3,11 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Callable
+from typing import Generic
 from typing import Iterable
 from typing import Literal
 from typing import Sequence
+from typing import TypeVar
 
 from narwhals.dependencies import is_numpy_array
 from narwhals.utils import flatten
@@ -48,9 +50,11 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [4, 5]})
             >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [4, 5]})
+            >>> df_pa = pa.table({"a": [1, 2], "b": [4, 5]})
 
             Let's define a dataframe-agnostic function:
 
@@ -58,7 +62,7 @@ class Expr:
             ... def func(df):
             ...     return df.select((nw.col("b") + 10).alias("c"))
 
-            We can then pass either pandas or Polars to `func`:
+            We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                 c
@@ -74,6 +78,12 @@ class Expr:
             │ 14  │
             │ 15  │
             └─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            c: int64
+            ----
+            c: [[14,15]]
+
         """
         return self.__class__(lambda plx: self._call(plx).alias(name))
 
@@ -84,10 +94,12 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> data = {"a": [1, 2, 3, 4]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Lets define a library-agnostic function:
 
@@ -95,7 +107,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").pipe(lambda x: x + 1))
 
-            We can then pass any supported library:
+            We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a
@@ -115,6 +127,11 @@ class Expr:
             │ 4   │
             │ 5   │
             └─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[2,3,4,5]]
         """
         return function(self, *args, **kwargs)
 
@@ -131,10 +148,12 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> from datetime import date
             >>> df_pd = pd.DataFrame({"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0]})
             >>> df_pl = pl.DataFrame({"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0]})
+            >>> df_pa = pa.table({"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0]})
 
             Let's define a dataframe-agnostic function:
 
@@ -144,7 +163,7 @@ class Expr:
             ...         nw.col("foo").cast(nw.Float32), nw.col("bar").cast(nw.UInt8)
             ...     )
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                foo  bar
@@ -162,6 +181,13 @@ class Expr:
             │ 2.0 ┆ 7   │
             │ 3.0 ┆ 8   │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            foo: float
+            bar: uint8
+            ----
+            foo: [[1,2,3]]
+            bar: [[6,7,8]]
         """
         return self.__class__(
             lambda plx: self._call(plx).cast(dtype),
@@ -299,9 +325,11 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [True, False], "b": [True, True]})
             >>> df_pl = pl.DataFrame({"a": [True, False], "b": [True, True]})
+            >>> df_pa = pa.table({"a": [True, False], "b": [True, True]})
 
             We define a dataframe-agnostic function:
 
@@ -309,7 +337,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").any())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                   a     b
@@ -323,6 +351,13 @@ class Expr:
             ╞══════╪══════╡
             │ true ┆ true │
             └──────┴──────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: bool
+            b: bool
+            ----
+            a: [[true]]
+            b: [[true]]
         """
         return self.__class__(lambda plx: self._call(plx).any())
 
@@ -333,9 +368,11 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [True, False], "b": [True, True]})
             >>> df_pl = pl.DataFrame({"a": [True, False], "b": [True, True]})
+            >>> df_pa = pa.table({"a": [True, False], "b": [True, True]})
 
             Let's define a dataframe-agnostic function:
 
@@ -343,7 +380,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").all())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                    a     b
@@ -357,6 +394,13 @@ class Expr:
             ╞═══════╪══════╡
             │ false ┆ true │
             └───────┴──────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: bool
+            b: bool
+            ----
+            a: [[false]]
+            b: [[true]]
         """
         return self.__class__(lambda plx: self._call(plx).all())
 
@@ -379,7 +423,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").mean())
 
-            We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                  a    b
@@ -414,9 +458,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [20, 25, 60], "b": [1.5, 1, -1.4]})
             >>> df_pl = pl.DataFrame({"a": [20, 25, 60], "b": [1.5, 1, -1.4]})
+            >>> df_pa = pa.table({"a": [20, 25, 60], "b": [1.5, 1, -1.4]})
 
             Let's define a dataframe-agnostic function:
 
@@ -424,7 +470,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").std(ddof=0))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                       a         b
@@ -438,6 +484,13 @@ class Expr:
             ╞══════════╪══════════╡
             │ 17.79513 ┆ 1.265789 │
             └──────────┴──────────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: double
+            b: double
+            ----
+            a: [[17.795130420052185]]
+            b: [[1.2657891697365016]]
 
         """
         return self.__class__(lambda plx: self._call(plx).std(ddof=ddof))
@@ -449,9 +502,11 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [5, 10], "b": [50, 100]})
             >>> df_pl = pl.DataFrame({"a": [5, 10], "b": [50, 100]})
+            >>> df_pa = pa.table({"a": [5, 10], "b": [50, 100]})
 
             Let's define a dataframe-agnostic function:
 
@@ -459,7 +514,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").sum())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                 a    b
@@ -473,6 +528,13 @@ class Expr:
             ╞═════╪═════╡
             │ 15  ┆ 150 │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[15]]
+            b: [[150]]
         """
         return self.__class__(lambda plx: self._call(plx).sum())
 
@@ -483,9 +545,11 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [4, 3]})
             >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [4, 3]})
+            >>> df_pa = pa.table({"a": [1, 2], "b": [4, 3]})
 
             Let's define a dataframe-agnostic function:
 
@@ -493,7 +557,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.min("a", "b"))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a  b
@@ -507,6 +571,13 @@ class Expr:
             ╞═════╪═════╡
             │ 1   ┆ 3   │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[1]]
+            b: [[3]]
         """
         return self.__class__(lambda plx: self._call(plx).min())
 
@@ -517,9 +588,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [10, 20], "b": [50, 100]})
             >>> df_pl = pl.DataFrame({"a": [10, 20], "b": [50, 100]})
+            >>> df_pa = pa.table({"a": [10, 20], "b": [50, 100]})
 
             Let's define a dataframe-agnostic function:
 
@@ -527,7 +600,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.max("a", "b"))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                 a    b
@@ -541,6 +614,13 @@ class Expr:
             ╞═════╪═════╡
             │ 20  ┆ 100 │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[20]]
+            b: [[100]]
         """
         return self.__class__(lambda plx: self._call(plx).max())
 
@@ -551,9 +631,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [None, 4, 4]})
             >>> df_pl = pl.DataFrame({"a": [1, 2, 3], "b": [None, 4, 4]})
+            >>> df_pa = pa.table({"a": [1, 2, 3], "b": [None, 4, 4]})
 
             Let's define a dataframe-agnostic function:
 
@@ -561,7 +643,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.all().count())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a  b
@@ -575,6 +657,13 @@ class Expr:
             ╞═════╪═════╡
             │ 3   ┆ 2   │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[3]]
+            b: [[2]]
         """
         return self.__class__(lambda plx: self._call(plx).count())
 
@@ -585,9 +674,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 2, 3, 4, 5], "b": [1, 1, 3, 3, 5]})
             >>> df_pl = pl.DataFrame({"a": [1, 2, 3, 4, 5], "b": [1, 1, 3, 3, 5]})
+            >>> df_pa = pa.table({"a": [1, 2, 3, 4, 5], "b": [1, 1, 3, 3, 5]})
 
             Let's define a dataframe-agnostic function:
 
@@ -595,7 +686,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").n_unique())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a  b
@@ -609,6 +700,13 @@ class Expr:
             ╞═════╪═════╡
             │ 5   ┆ 3   │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[5]]
+            b: [[3]]
         """
         return self.__class__(lambda plx: self._call(plx).n_unique())
 
@@ -619,9 +717,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 1, 3, 5, 5], "b": [2, 4, 4, 6, 6]})
             >>> df_pl = pl.DataFrame({"a": [1, 1, 3, 5, 5], "b": [2, 4, 4, 6, 6]})
+            >>> df_pa = pa.table({"a": [1, 1, 3, 5, 5], "b": [2, 4, 4, 6, 6]})
 
             Let's define a dataframe-agnostic function:
 
@@ -629,7 +729,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").unique())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a  b
@@ -647,6 +747,13 @@ class Expr:
             │ 3   ┆ 4   │
             │ 5   ┆ 6   │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[1,3,5]]
+            b: [[2,4,6]]
         """
         return self.__class__(lambda plx: self._call(plx).unique())
 
@@ -657,10 +764,12 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> data = {"a": [1, -2], "b": [-3, 4]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -668,7 +777,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").abs())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a  b
@@ -684,6 +793,13 @@ class Expr:
             │ 1   ┆ 3   │
             │ 2   ┆ 4   │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[1,2]]
+            b: [[3,4]]
         """
         return self.__class__(lambda plx: self._call(plx).abs())
 
@@ -694,9 +810,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 1, 3, 5, 5], "b": [2, 4, 4, 6, 6]})
             >>> df_pl = pl.DataFrame({"a": [1, 1, 3, 5, 5], "b": [2, 4, 4, 6, 6]})
+            >>> df_pa = pa.table({"a": [1, 1, 3, 5, 5], "b": [2, 4, 4, 6, 6]})
 
             Let's define a dataframe-agnostic function:
 
@@ -704,7 +822,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").cum_sum())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                 a   b
@@ -726,6 +844,13 @@ class Expr:
             │ 10  ┆ 16  │
             │ 15  ┆ 22  │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[1,2,5,10,15]]
+            b: [[2,6,10,16,22]]
         """
         return self.__class__(lambda plx: self._call(plx).cum_sum())
 
@@ -745,9 +870,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 1, 3, 5, 5]})
             >>> df_pl = pl.DataFrame({"a": [1, 1, 3, 5, 5]})
+            >>> df_pa = pa.table({"a": [1, 1, 3, 5, 5]})
 
             Let's define a dataframe-agnostic function:
 
@@ -755,7 +882,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(a_diff=nw.col("a").diff())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a_diff
@@ -777,6 +904,11 @@ class Expr:
             │ 2      │
             │ 0      │
             └────────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a_diff: int64
+            ----
+            a_diff: [[null,0,2,2,0]]
         """
         return self.__class__(lambda plx: self._call(plx).diff())
 
@@ -796,9 +928,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 1, 3, 5, 5]})
             >>> df_pl = pl.DataFrame({"a": [1, 1, 3, 5, 5]})
+            >>> df_pa = pa.table({"a": [1, 1, 3, 5, 5]})
 
             Let's define a dataframe-agnostic function:
 
@@ -806,7 +940,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(a_shift=nw.col("a").shift(n=1))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a_shift
@@ -828,6 +962,11 @@ class Expr:
             │ 3       │
             │ 5       │
             └─────────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a_shift: int64
+            ----
+            a_shift: [[null,1,1,3,5]]
         """
         return self.__class__(lambda plx: self._call(plx).shift(n))
 
@@ -843,9 +982,10 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
-
+            >>> import pyarrow as pa
             >>> df_pd = pd.DataFrame({"a": [5, None, 1, 2]})
             >>> df_pl = pl.DataFrame({"a": [5, None, 1, 2]})
+            >>> df_pa = pa.table({"a": [5, None, 1, 2]})
 
             Let's define dataframe-agnostic functions:
 
@@ -858,7 +998,7 @@ class Expr:
             ...     df = df.select(nw.col("a").sort(descending=True))
             ...     return nw.to_native(df)
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                  a
@@ -878,6 +1018,11 @@ class Expr:
             │ 2    │
             │ 5    │
             └──────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[null,1,2,5]]
 
             >>> func_descend(df_pd)
                  a
@@ -897,6 +1042,11 @@ class Expr:
             │ 2    │
             │ 1    │
             └──────┘
+            >>> func_descend(df_pa)
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[null,5,2,1]]
         """
         return self.__class__(
             lambda plx: self._call(plx).sort(descending=descending, nulls_last=nulls_last)
@@ -919,9 +1069,11 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 2, 3, 4, 5]})
             >>> df_pl = pl.DataFrame({"a": [1, 2, 3, 4, 5]})
+            >>> df_pa = pa.table({"a": [1, 2, 3, 4, 5]})
 
             Let's define a dataframe-agnostic function:
 
@@ -929,7 +1081,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").is_between(2, 4, "right"))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                    a
@@ -951,6 +1103,11 @@ class Expr:
             │ true  │
             │ false │
             └───────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: bool
+            ----
+            a: [[false,false,true,true,false]]
         """
         return self.__class__(
             lambda plx: self._call(plx).is_between(lower_bound, upper_bound, closed)
@@ -966,9 +1123,11 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [1, 2, 9, 10]})
             >>> df_pl = pl.DataFrame({"a": [1, 2, 9, 10]})
+            >>> df_pa = pa.table({"a": [1, 2, 9, 10]})
 
             Let's define a dataframe-agnostic function:
 
@@ -976,7 +1135,7 @@ class Expr:
             ... def func(df):
             ...     return df.with_columns(b=nw.col("a").is_in([1, 2]))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                 a      b
@@ -997,6 +1156,13 @@ class Expr:
             │ 9   ┆ false │
             │ 10  ┆ false │
             └─────┴───────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: bool
+            ----
+            a: [[1,2,9,10]]
+            b: [[true,true,false,false]]
         """
         if isinstance(other, Iterable) and not isinstance(other, (str, bytes)):
             return self.__class__(lambda plx: self._call(plx).is_in(other))
@@ -1011,9 +1177,11 @@ class Expr:
         Examples:
             >>> import polars as pl
             >>> import pandas as pd
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame({"a": [2, 3, 4, 5, 6, 7], "b": [10, 11, 12, 13, 14, 15]})
             >>> df_pl = pl.DataFrame({"a": [2, 3, 4, 5, 6, 7], "b": [10, 11, 12, 13, 14, 15]})
+            >>> df_pa = pa.table({"a": [2, 3, 4, 5, 6, 7], "b": [10, 11, 12, 13, 14, 15]})
 
             Let's define a dataframe-agnostic function:
 
@@ -1024,7 +1192,7 @@ class Expr:
             ...         nw.col("b").filter(nw.col("b") < 13),
             ...     )
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a   b
@@ -1042,6 +1210,13 @@ class Expr:
             │ 6   ┆ 11  │
             │ 7   ┆ 12  │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[5,6,7]]
+            b: [[10,11,12]]
         """
         return self.__class__(
             lambda plx: self._call(plx).filter(
@@ -1060,6 +1235,7 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame(
             ...     {"a": [2, 4, None, 3, 5], "b": [2.0, 4.0, float("nan"), 3.0, 5.0]}
@@ -1067,6 +1243,9 @@ class Expr:
             >>> df_pl = pl.DataFrame(
             ...     {"a": [2, 4, None, 3, 5], "b": [2.0, 4.0, float("nan"), 3.0, 5.0]}
             ... )
+            >>> df_pa = pa.table(
+            ...     {"a": [2, 4, None, 3, 5], "b": [2.0, 4.0, float("nan"), 3.0, 5.0]}
+            ... )
 
             Let's define a dataframe-agnostic function:
 
@@ -1076,7 +1255,7 @@ class Expr:
             ...         a_is_null=nw.col("a").is_null(), b_is_null=nw.col("b").is_null()
             ...     )
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                  a    b  a_is_null  b_is_null
@@ -1099,6 +1278,18 @@ class Expr:
             │ 3    ┆ 3.0 ┆ false     ┆ false     │
             │ 5    ┆ 5.0 ┆ false     ┆ false     │
             └──────┴─────┴───────────┴───────────┘
+
+            >>> func(df_pa)  # nan != null for pyarrow
+            pyarrow.Table
+            a: int64
+            b: double
+            a_is_null: bool
+            b_is_null: bool
+            ----
+            a: [[2,4,null,3,5]]
+            b: [[2,4,nan,3,5]]
+            a_is_null: [[false,false,true,false,false]]
+            b_is_null: [[false,false,false,false,false]]
         """
         return self.__class__(lambda plx: self._call(plx).is_null())
 
@@ -1109,10 +1300,12 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> data = {"a": [1, None, None, 2]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             We define a library agnostic function:
 
@@ -1120,7 +1313,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").is_null().arg_true())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a
@@ -1136,6 +1329,11 @@ class Expr:
             │ 1   │
             │ 2   │
             └─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[1,2]]
         """
         return self.__class__(lambda plx: self._call(plx).arg_true())
 
@@ -1150,6 +1348,7 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df_pd = pd.DataFrame(
             ...     {"a": [2, 4, None, 3, 5], "b": [2.0, 4.0, float("nan"), 3.0, 5.0]}
@@ -1157,6 +1356,9 @@ class Expr:
             >>> df_pl = pl.DataFrame(
             ...     {"a": [2, 4, None, 3, 5], "b": [2.0, 4.0, float("nan"), 3.0, 5.0]}
             ... )
+            >>> df_pa = pa.table(
+            ...     {"a": [2, 4, None, 3, 5], "b": [2.0, 4.0, float("nan"), 3.0, 5.0]}
+            ... )
 
             Let's define a dataframe-agnostic function:
 
@@ -1164,7 +1366,7 @@ class Expr:
             ... def func(df):
             ...     return df.with_columns(nw.col("a", "b").fill_null(0))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                  a    b
@@ -1187,6 +1389,14 @@ class Expr:
             │ 3   ┆ 3.0 │
             │ 5   ┆ 5.0 │
             └─────┴─────┘
+
+            >>> func(df_pa)  # nan != null for pyarrow
+            pyarrow.Table
+            a: int64
+            b: double
+            ----
+            a: [[2,4,0,3,5]]
+            b: [[2,4,nan,3,5]]
         """
         return self.__class__(lambda plx: self._call(plx).fill_null(value))
 
@@ -1203,9 +1413,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
 
             >>> df_pd = pd.DataFrame({"a": [2.0, 4.0, float("nan"), 3.0, None, 5.0]})
             >>> df_pl = pl.DataFrame({"a": [2.0, 4.0, float("nan"), 3.0, None, 5.0]})
+            >>> df_pa = pa.table({"a": [2.0, 4.0, float("nan"), 3.0, None, 5.0]})
 
             Let's define a dataframe-agnostic function:
 
@@ -1213,7 +1425,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").drop_nulls())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                  a
@@ -1234,6 +1446,11 @@ class Expr:
             │ 3.0 │
             │ 5.0 │
             └─────┘
+            >>> func(df_pa)  # nan != null for pyarrow
+            pyarrow.Table
+            a: double
+            ----
+            a: [[2,4,nan,3,5]]
         """
         return self.__class__(lambda plx: self._call(plx).drop_nulls())
 
@@ -1259,9 +1476,10 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
-
+            >>> import pyarrow as pa
             >>> df_pd = pd.DataFrame({"a": [1, 2, 3]})
             >>> df_pl = pl.DataFrame({"a": [1, 2, 3]})
+            >>> df_pa = pa.table({"a": [1, 2, 3]})
 
             Let's define a dataframe-agnostic function:
 
@@ -1269,7 +1487,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").sample(fraction=1.0, with_replacement=True))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)  # doctest: +SKIP
                a
@@ -1287,6 +1505,11 @@ class Expr:
             │ 3   │
             │ 3   │
             └─────┘
+            >>> func(df_pa)  # doctest: +SKIP
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[1,3,3]]
         """
         return self.__class__(
             lambda plx: self._call(plx).sample(
@@ -1307,9 +1530,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": [1, 2, 3], "b": [1, 1, 2]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -1317,7 +1542,7 @@ class Expr:
             ... def func(df):
             ...     return df.with_columns(a_min_per_group=nw.col("a").min().over("b"))
 
-            We can then pass either pandas or Polars:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a  b  a_min_per_group
@@ -1335,6 +1560,15 @@ class Expr:
             │ 2   ┆ 1   ┆ 1               │
             │ 3   ┆ 2   ┆ 3               │
             └─────┴─────┴─────────────────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            a_min_per_group: int64
+            ----
+            a: [[1,2,3]]
+            b: [[1,1,2]]
+            a_min_per_group: [[1,1,3]]
         """
         return self.__class__(lambda plx: self._call(plx).over(flatten(keys)))
 
@@ -1346,9 +1580,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": [1, 2, 3, 1], "b": ["a", "a", "b", "c"]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -1356,7 +1592,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.all().is_duplicated())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                    a      b
@@ -1376,6 +1612,13 @@ class Expr:
             │ false ┆ false │
             │ true  ┆ false │
             └───────┴───────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: bool
+            b: bool
+            ----
+            a: [[true,false,false,true]]
+            b: [[true,true,false,false]]
         """
         return self.__class__(lambda plx: self._call(plx).is_duplicated())
 
@@ -1387,9 +1630,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": [1, 2, 3, 1], "b": ["a", "a", "b", "c"]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -1397,7 +1642,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.all().is_unique())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                    a      b
@@ -1417,6 +1662,13 @@ class Expr:
             │ true  ┆ true  │
             │ false ┆ true  │
             └───────┴───────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: bool
+            b: bool
+            ----
+            a: [[false,true,true,false]]
+            b: [[false,false,true,true]]
         """
         return self.__class__(lambda plx: self._call(plx).is_unique())
 
@@ -1432,9 +1684,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": [1, 2, None, 1], "b": ["a", None, "b", None]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -1442,7 +1696,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.all().null_count())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a  b
@@ -1456,6 +1710,13 @@ class Expr:
             ╞═════╪═════╡
             │ 1   ┆ 2   │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            b: int64
+            ----
+            a: [[1]]
+            b: [[2]]
         """
         return self.__class__(lambda plx: self._call(plx).null_count())
 
@@ -1467,9 +1728,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": [1, 2, 3, 1], "b": ["a", "a", "b", "c"]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -1477,7 +1740,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.all().is_first_distinct())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                    a      b
@@ -1497,6 +1760,13 @@ class Expr:
             │ true  ┆ true  │
             │ false ┆ true  │
             └───────┴───────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: bool
+            b: bool
+            ----
+            a: [[true,true,true,false]]
+            b: [[true,false,true,true]]
         """
         return self.__class__(lambda plx: self._call(plx).is_first_distinct())
 
@@ -1507,9 +1777,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": [1, 2, 3, 1], "b": ["a", "a", "b", "c"]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -1517,7 +1789,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.all().is_last_distinct())
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                    a      b
@@ -1537,6 +1809,13 @@ class Expr:
             │ true  ┆ true  │
             │ true  ┆ true  │
             └───────┴───────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: bool
+            b: bool
+            ----
+            a: [[false,true,true,true]]
+            b: [[false,true,true,true]]
         """
         return self.__class__(lambda plx: self._call(plx).is_last_distinct())
 
@@ -1562,9 +1841,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": list(range(50)), "b": list(range(50, 100))}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function:
 
@@ -1572,7 +1853,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a", "b").quantile(0.5, interpolation="linear"))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                   a     b
@@ -1587,6 +1868,13 @@ class Expr:
             ╞══════╪══════╡
             │ 24.5 ┆ 74.5 │
             └──────┴──────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: double
+            b: double
+            ----
+            a: [[24.5]]
+            b: [[74.5]]
         """
         return self.__class__(
             lambda plx: self._call(plx).quantile(quantile, interpolation)
@@ -1603,9 +1891,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": list(range(10))}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function that returns the first 3 rows:
 
@@ -1613,7 +1903,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").head(3))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a
@@ -1631,6 +1921,11 @@ class Expr:
             │ 1   │
             │ 2   │
             └─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[0,1,2]]
         """
         return self.__class__(lambda plx: self._call(plx).head(n))
 
@@ -1645,9 +1940,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": list(range(10))}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function that returns the last 3 rows:
 
@@ -1655,7 +1952,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").tail(3))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a
@@ -1673,6 +1970,11 @@ class Expr:
             │ 8   │
             │ 9   │
             └─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[7,8,9]]
         """
         return self.__class__(lambda plx: self._call(plx).tail(n))
 
@@ -1696,9 +1998,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": [1.12345, 2.56789, 3.901234]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function that rounds to the first decimal:
 
@@ -1706,7 +2010,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").round(1))
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                  a
@@ -1724,6 +2028,11 @@ class Expr:
             │ 2.6 │
             │ 3.9 │
             └─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: double
+            ----
+            a: [[1.1,2.6,3.9]]
         """
         return self.__class__(lambda plx: self._call(plx).round(decimals))
 
@@ -1737,9 +2046,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": ["x", "y", "z"], "b": [1, 2, 1]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function that computes the len over different values of "b" column:
 
@@ -1750,7 +2061,7 @@ class Expr:
             ...         nw.col("a").filter(nw.col("b") == 2).len().alias("a2"),
             ...     )
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a1  a2
@@ -1764,6 +2075,13 @@ class Expr:
             ╞═════╪═════╡
             │ 2   ┆ 1   │
             └─────┴─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a1: int64
+            a2: int64
+            ----
+            a1: [[2]]
+            a2: [[1]]
         """
         return self.__class__(lambda plx: self._call(plx).len())
 
@@ -1779,9 +2097,11 @@ class Expr:
             >>> import narwhals as nw
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> data = {"a": [1, 2, 3, 4], "b": [5, 6, 7, 8]}
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             Let's define a dataframe-agnostic function in which gather every 2 rows,
             starting from a offset of 1:
@@ -1790,11 +2110,12 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").gather_every(n=2, offset=1))
 
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
+
             >>> func(df_pd)
                a
             1  2
             3  4
-
             >>> func(df_pl)
             shape: (2, 1)
             ┌─────┐
@@ -1805,6 +2126,11 @@ class Expr:
             │ 2   │
             │ 4   │
             └─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[2,4]]
         """
         return self.__class__(
             lambda plx: self._call(plx).gather_every(n=n, offset=offset)
@@ -1827,11 +2153,13 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
 
             >>> s = [1, 2, 3]
             >>> df_pd = pd.DataFrame({"s": s})
             >>> df_pl = pl.DataFrame({"s": s})
+            >>> df_pa = pa.table({"s": s})
 
             We define a library agnostic function:
 
@@ -1839,7 +2167,7 @@ class Expr:
             ... def func_lower(df):
             ...     return df.select(nw.col("s").clip(2))
 
-            We can then pass either pandas or Polars to `func_lower`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func_lower`:
 
             >>> func_lower(df_pd)
                s
@@ -1857,6 +2185,11 @@ class Expr:
             │ 2   │
             │ 3   │
             └─────┘
+            >>> func_lower(df_pa)
+            pyarrow.Table
+            s: int64
+            ----
+            s: [[2,2,3]]
 
             We define another library agnostic function:
 
@@ -1864,7 +2197,7 @@ class Expr:
             ... def func_upper(df):
             ...     return df.select(nw.col("s").clip(upper_bound=2))
 
-            We can then pass either pandas or Polars to `func_upper`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func_upper`:
 
             >>> func_upper(df_pd)
                s
@@ -1882,12 +2215,18 @@ class Expr:
             │ 2   │
             │ 2   │
             └─────┘
+            >>> func_upper(df_pa)
+            pyarrow.Table
+            s: int64
+            ----
+            s: [[1,2,2]]
 
             We can have both at the same time
 
             >>> s = [-1, 1, -3, 3, -5, 5]
             >>> df_pd = pd.DataFrame({"s": s})
             >>> df_pl = pl.DataFrame({"s": s})
+            >>> df_pa = pa.table({"s": s})
 
             We define a library agnostic function:
 
@@ -1895,7 +2234,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("s").clip(-1, 3))
 
-            We can pass either pandas or Polars to `func`:
+            We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                s
@@ -1919,6 +2258,11 @@ class Expr:
             │ -1  │
             │ 3   │
             └─────┘
+            >>> func(df_pa)
+            pyarrow.Table
+            s: int64
+            ----
+            s: [[-1,1,-1,3,-1,3]]
         """
         return self.__class__(lambda plx: self._call(plx).clip(lower_bound, upper_bound))
 
@@ -1930,6 +2274,7 @@ class Expr:
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
 
             >>> data = {
@@ -1938,6 +2283,7 @@ class Expr:
             ... }
             >>> df_pd = pd.DataFrame(data)
             >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
 
             We define a library agnostic function:
 
@@ -1945,7 +2291,7 @@ class Expr:
             ... def func(df):
             ...     return df.select(nw.col("a").mode()).sort("a")
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
             >>> func(df_pd)
                a
@@ -1960,31 +2306,40 @@ class Expr:
             ╞═════╡
             │ 1   │
             └─────┘
+
+            >>> func(df_pa)
+            pyarrow.Table
+            a: int64
+            ----
+            a: [[1]]
         """
         return self.__class__(lambda plx: self._call(plx).mode())
 
     @property
-    def str(self: Self) -> ExprStringNamespace:
+    def str(self: Self) -> ExprStringNamespace[Self]:
         return ExprStringNamespace(self)
 
     @property
-    def dt(self: Self) -> ExprDateTimeNamespace:
+    def dt(self: Self) -> ExprDateTimeNamespace[Self]:
         return ExprDateTimeNamespace(self)
 
     @property
-    def cat(self: Self) -> ExprCatNamespace:
+    def cat(self: Self) -> ExprCatNamespace[Self]:
         return ExprCatNamespace(self)
 
     @property
-    def name(self: Self) -> ExprNameNamespace:
+    def name(self: Self) -> ExprNameNamespace[Self]:
         return ExprNameNamespace(self)
 
 
-class ExprCatNamespace:
-    def __init__(self, expr: Expr) -> None:
+T = TypeVar("T", bound=Expr)
+
+
+class ExprCatNamespace(Generic[T]):
+    def __init__(self: Self, expr: T) -> None:
         self._expr = expr
 
-    def get_categories(self) -> Expr:
+    def get_categories(self: Self) -> T:
         """
         Get unique categories from column.
 
@@ -2027,11 +2382,11 @@ class ExprCatNamespace:
         )
 
 
-class ExprStringNamespace:
-    def __init__(self, expr: Expr) -> None:
+class ExprStringNamespace(Generic[T]):
+    def __init__(self: Self, expr: T) -> None:
         self._expr = expr
 
-    def len_chars(self) -> Expr:
+    def len_chars(self: Self) -> T:
         r"""
         Return the length of each string as the number of characters.
 
@@ -2077,7 +2432,7 @@ class ExprStringNamespace:
 
     def replace(
         self, pattern: str, value: str, *, literal: bool = False, n: int = 1
-    ) -> Expr:
+    ) -> T:
         r"""
         Replace first matching regex/literal substring with a new string value.
 
@@ -2117,7 +2472,7 @@ class ExprStringNamespace:
             )
         )
 
-    def replace_all(self, pattern: str, value: str, *, literal: bool = False) -> Expr:
+    def replace_all(self: Self, pattern: str, value: str, *, literal: bool = False) -> T:
         r"""
         Replace all matching regex/literal substring with a new string value.
 
@@ -2156,7 +2511,7 @@ class ExprStringNamespace:
             )
         )
 
-    def strip_chars(self, characters: str | None = None) -> Expr:
+    def strip_chars(self: Self, characters: str | None = None) -> T:
         r"""
         Remove leading and trailing characters.
 
@@ -2190,7 +2545,7 @@ class ExprStringNamespace:
             lambda plx: self._expr._call(plx).str.strip_chars(characters)
         )
 
-    def starts_with(self, prefix: str) -> Expr:
+    def starts_with(self: Self, prefix: str) -> T:
         r"""
         Check if string values start with a substring.
 
@@ -2235,7 +2590,7 @@ class ExprStringNamespace:
             lambda plx: self._expr._call(plx).str.starts_with(prefix)
         )
 
-    def ends_with(self, suffix: str) -> Expr:
+    def ends_with(self: Self, suffix: str) -> T:
         r"""
         Check if string values end with a substring.
 
@@ -2280,7 +2635,7 @@ class ExprStringNamespace:
             lambda plx: self._expr._call(plx).str.ends_with(suffix)
         )
 
-    def contains(self, pattern: str, *, literal: bool = False) -> Expr:
+    def contains(self: Self, pattern: str, *, literal: bool = False) -> T:
         r"""
         Check if string contains a substring that matches a pattern.
 
@@ -2336,7 +2691,7 @@ class ExprStringNamespace:
             lambda plx: self._expr._call(plx).str.contains(pattern, literal=literal)
         )
 
-    def slice(self, offset: int, length: int | None = None) -> Expr:
+    def slice(self: Self, offset: int, length: int | None = None) -> T:
         r"""
         Create subslices of the string values of an expression.
 
@@ -2411,7 +2766,7 @@ class ExprStringNamespace:
             lambda plx: self._expr._call(plx).str.slice(offset=offset, length=length)
         )
 
-    def head(self, n: int = 5) -> Expr:
+    def head(self: Self, n: int = 5) -> T:
         r"""
         Take the first n elements of each string.
 
@@ -2459,7 +2814,7 @@ class ExprStringNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).str.slice(0, n))
 
-    def tail(self, n: int = 5) -> Expr:
+    def tail(self: Self, n: int = 5) -> T:
         r"""
         Take the last n elements of each string.
 
@@ -2507,7 +2862,7 @@ class ExprStringNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).str.slice(-n))
 
-    def to_datetime(self: Self, format: str | None = None) -> Expr:  # noqa: A002
+    def to_datetime(self: Self, format: str | None = None) -> T:  # noqa: A002
         """
         Convert to Datetime dtype.
 
@@ -2567,7 +2922,7 @@ class ExprStringNamespace:
             lambda plx: self._expr._call(plx).str.to_datetime(format=format)
         )
 
-    def to_uppercase(self) -> Expr:
+    def to_uppercase(self: Self) -> T:
         r"""
         Transform string to uppercase variant.
 
@@ -2613,7 +2968,7 @@ class ExprStringNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).str.to_uppercase())
 
-    def to_lowercase(self) -> Expr:
+    def to_lowercase(self: Self) -> T:
         r"""
         Transform string to lowercase variant.
 
@@ -2654,11 +3009,11 @@ class ExprStringNamespace:
         return self._expr.__class__(lambda plx: self._expr._call(plx).str.to_lowercase())
 
 
-class ExprDateTimeNamespace:
-    def __init__(self, expr: Expr) -> None:
+class ExprDateTimeNamespace(Generic[T]):
+    def __init__(self: Self, expr: T) -> None:
         self._expr = expr
 
-    def date(self) -> Expr:
+    def date(self: Self) -> T:
         """
         Extract the date from underlying DateTime representation.
 
@@ -2700,7 +3055,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.date())
 
-    def year(self) -> Expr:
+    def year(self: Self) -> T:
         """
         Extract year from underlying DateTime representation.
 
@@ -2748,7 +3103,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.year())
 
-    def month(self) -> Expr:
+    def month(self: Self) -> T:
         """
         Extract month from underlying DateTime representation.
 
@@ -2799,7 +3154,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.month())
 
-    def day(self) -> Expr:
+    def day(self: Self) -> T:
         """
         Extract day from underlying DateTime representation.
 
@@ -2851,7 +3206,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.day())
 
-    def hour(self) -> Expr:
+    def hour(self: Self) -> T:
         """
         Extract hour from underlying DateTime representation.
 
@@ -2899,7 +3254,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.hour())
 
-    def minute(self) -> Expr:
+    def minute(self: Self) -> T:
         """
         Extract minutes from underlying DateTime representation.
 
@@ -2950,7 +3305,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.minute())
 
-    def second(self) -> Expr:
+    def second(self: Self) -> T:
         """
         Extract seconds from underlying DateTime representation.
 
@@ -3000,7 +3355,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.second())
 
-    def millisecond(self) -> Expr:
+    def millisecond(self: Self) -> T:
         """
         Extract milliseconds from underlying DateTime representation.
 
@@ -3051,7 +3406,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.millisecond())
 
-    def microsecond(self) -> Expr:
+    def microsecond(self: Self) -> T:
         """
         Extract microseconds from underlying DateTime representation.
 
@@ -3102,7 +3457,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.microsecond())
 
-    def nanosecond(self) -> Expr:
+    def nanosecond(self: Self) -> T:
         """
         Extract Nanoseconds from underlying DateTime representation
 
@@ -3153,7 +3508,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.nanosecond())
 
-    def ordinal_day(self) -> Expr:
+    def ordinal_day(self: Self) -> T:
         """
         Get ordinal day.
 
@@ -3191,7 +3546,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.ordinal_day())
 
-    def total_minutes(self) -> Expr:
+    def total_minutes(self: Self) -> T:
         """
         Get total minutes.
 
@@ -3234,7 +3589,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.total_minutes())
 
-    def total_seconds(self) -> Expr:
+    def total_seconds(self: Self) -> T:
         """
         Get total seconds.
 
@@ -3277,7 +3632,7 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.total_seconds())
 
-    def total_milliseconds(self) -> Expr:
+    def total_milliseconds(self: Self) -> T:
         """
         Get total milliseconds.
 
@@ -3329,7 +3684,7 @@ class ExprDateTimeNamespace:
             lambda plx: self._expr._call(plx).dt.total_milliseconds()
         )
 
-    def total_microseconds(self) -> Expr:
+    def total_microseconds(self: Self) -> T:
         """
         Get total microseconds.
 
@@ -3381,7 +3736,7 @@ class ExprDateTimeNamespace:
             lambda plx: self._expr._call(plx).dt.total_microseconds()
         )
 
-    def total_nanoseconds(self) -> Expr:
+    def total_nanoseconds(self: Self) -> T:
         """
         Get total nanoseconds.
 
@@ -3430,7 +3785,7 @@ class ExprDateTimeNamespace:
             lambda plx: self._expr._call(plx).dt.total_nanoseconds()
         )
 
-    def to_string(self, format: str) -> Expr:  # noqa: A002
+    def to_string(self: Self, format: str) -> T:  # noqa: A002
         """
         Convert a Date/Time/Datetime column into a String column with the given format.
 
@@ -3508,7 +3863,7 @@ class ExprDateTimeNamespace:
             lambda plx: self._expr._call(plx).dt.to_string(format)
         )
 
-    def replace_time_zone(self, time_zone: str | None) -> Expr:
+    def replace_time_zone(self: Self, time_zone: str | None) -> T:
         """
         Replace time zone.
 
@@ -3563,7 +3918,7 @@ class ExprDateTimeNamespace:
             lambda plx: self._expr._call(plx).dt.replace_time_zone(time_zone)
         )
 
-    def convert_time_zone(self, time_zone: str) -> Expr:
+    def convert_time_zone(self: Self, time_zone: str) -> T:
         """
         Convert to a new time zone.
 
@@ -3622,11 +3977,11 @@ class ExprDateTimeNamespace:
         )
 
 
-class ExprNameNamespace:
-    def __init__(self: Self, expr: Expr) -> None:
+class ExprNameNamespace(Generic[T]):
+    def __init__(self: Self, expr: T) -> None:
         self._expr = expr
 
-    def keep(self: Self) -> Expr:
+    def keep(self: Self) -> T:
         r"""
         Keep the original root name of the expression.
 
@@ -3658,7 +4013,7 @@ class ExprNameNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).name.keep())
 
-    def map(self: Self, function: Callable[[str], str]) -> Expr:
+    def map(self: Self, function: Callable[[str], str]) -> T:
         r"""
         Rename the output of an expression by mapping a function over the root name.
 
@@ -3694,7 +4049,7 @@ class ExprNameNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).name.map(function))
 
-    def prefix(self: Self, prefix: str) -> Expr:
+    def prefix(self: Self, prefix: str) -> T:
         r"""
         Add a prefix to the root column name of the expression.
 
@@ -3730,7 +4085,7 @@ class ExprNameNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).name.prefix(prefix))
 
-    def suffix(self: Self, suffix: str) -> Expr:
+    def suffix(self: Self, suffix: str) -> T:
         r"""
         Add a suffix to the root column name of the expression.
 
@@ -3765,7 +4120,7 @@ class ExprNameNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).name.suffix(suffix))
 
-    def to_lowercase(self: Self) -> Expr:
+    def to_lowercase(self: Self) -> T:
         r"""
         Make the root column name lowercase.
 
@@ -3797,7 +4152,7 @@ class ExprNameNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).name.to_lowercase())
 
-    def to_uppercase(self: Self) -> Expr:
+    def to_uppercase(self: Self) -> T:
         r"""
         Make the root column name uppercase.
 
@@ -3839,9 +4194,11 @@ def col(*names: str | Iterable[str]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [3, 4]})
         >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
+        >>> df_pa = pa.table({"a": [1, 2], "b": [3, 4]})
 
         We define a dataframe-agnostic function:
 
@@ -3849,7 +4206,7 @@ def col(*names: str | Iterable[str]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.col("a") * nw.col("b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a
@@ -3865,6 +4222,11 @@ def col(*names: str | Iterable[str]) -> Expr:
         │ 3   │
         │ 8   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[3,8]]
     """
 
     def func(plx: Any) -> Any:
@@ -3899,7 +4261,7 @@ def nth(*indices: int | Sequence[int]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.nth(0) * 2)
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a
@@ -3936,9 +4298,11 @@ def all_() -> Expr:
     Examples:
         >>> import polars as pl
         >>> import pandas as pd
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
         >>> df_pl = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+        >>> df_pa = pa.table({"a": [1, 2, 3], "b": [4, 5, 6]})
 
         Let's define a dataframe-agnostic function:
 
@@ -3946,7 +4310,7 @@ def all_() -> Expr:
         ... def func(df):
         ...     return df.select(nw.all() * 2)
 
-        We can then pass either pandas or Polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a   b
@@ -3964,6 +4328,13 @@ def all_() -> Expr:
         │ 4   ┆ 10  │
         │ 6   ┆ 12  │
         └─────┴─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        b: int64
+        ----
+        a: [[2,4,6]]
+        b: [[8,10,12]]
     """
     return Expr(lambda plx: plx.all())
 
@@ -3976,9 +4347,11 @@ def len_() -> Expr:
     Examples:
         >>> import polars as pl
         >>> import pandas as pd
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [5, 10]})
         >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [5, 10]})
+        >>> df_pa = pa.table({"a": [1, 2], "b": [5, 10]})
 
         Let's define a dataframe-agnostic function:
 
@@ -3986,7 +4359,7 @@ def len_() -> Expr:
         ... def func(df):
         ...     return df.select(nw.len())
 
-        We can then pass either pandas or Polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            len
@@ -4000,6 +4373,11 @@ def len_() -> Expr:
         ╞═════╡
         │ 2   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        len: int64
+        ----
+        len: [[2]]
     """
 
     def func(plx: Any) -> Any:
@@ -4021,9 +4399,11 @@ def sum(*columns: str) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> df_pl = pl.DataFrame({"a": [1, 2]})
         >>> df_pd = pd.DataFrame({"a": [1, 2]})
+        >>> df_pa = pa.table({"a": [1, 2]})
 
         We define a dataframe-agnostic function:
 
@@ -4031,7 +4411,7 @@ def sum(*columns: str) -> Expr:
         ... def func(df):
         ...     return df.select(nw.sum("a"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a
@@ -4045,6 +4425,11 @@ def sum(*columns: str) -> Expr:
         ╞═════╡
         │ 3   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[3]]
     """
 
     return Expr(lambda plx: plx.sum(*columns))
@@ -4112,9 +4497,11 @@ def min(*columns: str) -> Expr:
     Examples:
         >>> import polars as pl
         >>> import pandas as pd
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [5, 10]})
         >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [5, 10]})
+        >>> df_pa = pa.table({"a": [1, 2], "b": [5, 10]})
 
         Let's define a dataframe-agnostic function:
 
@@ -4122,7 +4509,7 @@ def min(*columns: str) -> Expr:
         ... def func(df):
         ...     return df.select(nw.min("b"))
 
-        We can then pass either pandas or Polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            b
@@ -4136,6 +4523,11 @@ def min(*columns: str) -> Expr:
         ╞═════╡
         │ 5   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        b: int64
+        ----
+        b: [[5]]
     """
     return Expr(lambda plx: plx.min(*columns))
 
@@ -4153,9 +4545,11 @@ def max(*columns: str) -> Expr:
     Examples:
         >>> import polars as pl
         >>> import pandas as pd
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [5, 10]})
         >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [5, 10]})
+        >>> df_pa = pa.table({"a": [1, 2], "b": [5, 10]})
 
         Let's define a dataframe-agnostic function:
 
@@ -4163,7 +4557,7 @@ def max(*columns: str) -> Expr:
         ... def func(df):
         ...     return df.select(nw.max("a"))
 
-        We can then pass either pandas or Polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a
@@ -4177,6 +4571,11 @@ def max(*columns: str) -> Expr:
         ╞═════╡
         │ 2   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[2]]
     """
     return Expr(lambda plx: plx.max(*columns))
 
@@ -4195,10 +4594,12 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> data = {"a": [1, 2, 3], "b": [5, 10, None]}
         >>> df_pl = pl.DataFrame(data)
         >>> df_pd = pd.DataFrame(data)
+        >>> df_pa = pa.table(data)
 
         We define a dataframe-agnostic function:
 
@@ -4206,7 +4607,7 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.sum_horizontal("a", "b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
               a
@@ -4224,6 +4625,11 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         │ 12  │
         │ 3   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[6,12,3]]
     """
     if not exprs:
         msg = "At least one expression must be passed to `sum_horizontal`"
@@ -4264,7 +4670,7 @@ def min_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.min_horizontal("a", "b"))
 
-        We can then pass either pandas, polars or pyarrow to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(pd.DataFrame(data))
              a
@@ -4327,7 +4733,7 @@ def max_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.max_horizontal("a", "b"))
 
-        We can then pass either pandas, polars or pyarrow to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(pd.DataFrame(data))
              a
@@ -4398,9 +4804,11 @@ def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> df_pl = pl.DataFrame({"a": [1, 2, 3], "b": [5, 10, 15]})
         >>> df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [5, 10, 15]})
+        >>> df_pa = pa.table({"a": [1, 2, 3], "b": [5, 10, 15]})
 
         We define a dataframe-agnostic function:
 
@@ -4410,7 +4818,7 @@ def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When:
         ...         nw.when(nw.col("a") < 3).then(5).otherwise(6).alias("a_when")
         ...     )
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a   b  a_when
@@ -4428,6 +4836,15 @@ def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When:
         │ 2   ┆ 10  ┆ 5      │
         │ 3   ┆ 15  ┆ 6      │
         └─────┴─────┴────────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        b: int64
+        a_when: int64
+        ----
+        a: [[1,2,3]]
+        b: [[5,10,15]]
+        a_when: [[5,5,6]]
     """
     return When(*predicates)
 
@@ -4445,6 +4862,7 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> data = {
         ...     "a": [False, False, True, True, False, None],
@@ -4452,6 +4870,7 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... }
         >>> df_pl = pl.DataFrame(data)
         >>> df_pd = pd.DataFrame(data)
+        >>> df_pa = pa.table(data)
 
         We define a dataframe-agnostic function:
 
@@ -4459,7 +4878,7 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select("a", "b", all=nw.all_horizontal("a", "b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
                a      b    all
@@ -4484,6 +4903,16 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         │ false ┆ null  ┆ false │
         │ null  ┆ null  ┆ null  │
         └───────┴───────┴───────┘
+
+        >>> func(df_pa)
+        pyarrow.Table
+        a: bool
+        b: bool
+        all: bool
+        ----
+        a: [[false,false,true,true,false,null]]
+        b: [[false,true,true,null,null,null]]
+        all: [[false,false,true,null,false,null]]
     """
     if not exprs:
         msg = "At least one expression must be passed to `all_horizontal`"
@@ -4506,9 +4935,11 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> df_pl = pl.DataFrame({"a": [1, 2]})
         >>> df_pd = pd.DataFrame({"a": [1, 2]})
+        >>> df_pa = pa.table({"a": [1, 2]})
 
         We define a dataframe-agnostic function:
 
@@ -4516,7 +4947,7 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
         ... def func(df):
         ...     return df.with_columns(nw.lit(3).alias("b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a  b
@@ -4532,7 +4963,13 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
         │ 1   ┆ 3   │
         │ 2   ┆ 3   │
         └─────┴─────┘
-
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        b: int64
+        ----
+        a: [[1,2]]
+        b: [[3,3]]
     """
     if is_numpy_array(value):
         msg = (
@@ -4561,6 +4998,7 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> data = {
         ...     "a": [False, False, True, True, False, None],
@@ -4568,6 +5006,7 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... }
         >>> df_pl = pl.DataFrame(data)
         >>> df_pd = pd.DataFrame(data)
+        >>> df_pa = pa.table(data)
 
         We define a dataframe-agnostic function:
 
@@ -4575,7 +5014,7 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select("a", "b", any=nw.any_horizontal("a", "b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
                a      b    any
@@ -4600,6 +5039,16 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         │ false ┆ null  ┆ null  │
         │ null  ┆ null  ┆ null  │
         └───────┴───────┴───────┘
+
+        >>> func(df_pa)
+        pyarrow.Table
+        a: bool
+        b: bool
+        any: bool
+        ----
+        a: [[false,false,true,true,false,null]]
+        b: [[false,true,true,null,null,null]]
+        any: [[false,true,true,true,null,null]]
     """
     if not exprs:
         msg = "At least one expression must be passed to `any_horizontal`"
@@ -4622,6 +5071,7 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> data = {
         ...     "a": [1, 8, 3],
@@ -4630,6 +5080,7 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... }
         >>> df_pl = pl.DataFrame(data)
         >>> df_pd = pd.DataFrame(data)
+        >>> df_pa = pa.table(data)
 
         We define a dataframe-agnostic function that computes the horizontal mean of "a"
         and "b" columns:
@@ -4638,13 +5089,14 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.mean_horizontal("a", "b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
              a
         0  2.5
         1  6.5
         2  3.0
+
         >>> func(df_pl)
         shape: (3, 1)
         ┌─────┐
@@ -4656,6 +5108,12 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         │ 6.5 │
         │ 3.0 │
         └─────┘
+
+        >>> func(df_pa)
+        pyarrow.Table
+        a: double
+        ----
+        a: [[2.5,6.5,3]]
     """
     if not exprs:
         msg = "At least one expression must be passed to `mean_horizontal`"
@@ -4714,7 +5172,7 @@ def concat_str(
         ...         ).alias("full_sentence")
         ...     )
 
-        We can then pass either pandas, Polars or PyArrow to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(pd.DataFrame(data))
           full_sentence
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 395da97c..b8dfffbe 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -294,6 +294,7 @@ def from_dict(
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals as nw
         >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
 
@@ -305,7 +306,7 @@ def from_dict(
         ...     native_namespace = nw.get_native_namespace(df)
         ...     return nw.from_dict(new_data, native_namespace=native_namespace)
 
-        Let's see what happens when passing pandas / Polars input:
+        Let's see what happens when passing Pandas, Polars or PyArrow input:
 
         >>> func(pd.DataFrame(data))
            c  d
@@ -321,6 +322,13 @@ def from_dict(
         │ 5   ┆ 1   │
         │ 2   ┆ 4   │
         └─────┴─────┘
+        >>> func(pa.table(data))
+        pyarrow.Table
+        c: int64
+        d: int64
+        ----
+        c: [[5,2]]
+        d: [[1,4]]
     """
     from narwhals import dtypes
 
diff --git a/narwhals/series.py b/narwhals/series.py
index 36ecf50f..ddaf009f 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -3,9 +3,11 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Callable
+from typing import Generic
 from typing import Iterator
 from typing import Literal
 from typing import Sequence
+from typing import TypeVar
 from typing import overload
 
 from narwhals.utils import parse_version
@@ -40,7 +42,7 @@ class Series:
         return DataFrame
 
     def __init__(
-        self,
+        self: Self,
         series: Any,
         *,
         level: Literal["full", "interchange"],
@@ -52,16 +54,16 @@ class Series:
             msg = f"Expected Polars Series or an object which implements `__narwhals_series__`, got: {type(series)}."
             raise AssertionError(msg)
 
-    def __array__(self, dtype: Any = None, copy: bool | None = None) -> np.ndarray:
+    def __array__(self: Self, dtype: Any = None, copy: bool | None = None) -> np.ndarray:
         return self._compliant_series.__array__(dtype=dtype, copy=copy)
 
     @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._compliant_series[idx]
         return self._from_compliant_series(self._compliant_series[idx])
@@ -2491,23 +2493,26 @@ class Series:
         yield from self._compliant_series.__iter__()
 
     @property
-    def str(self) -> SeriesStringNamespace:
+    def str(self: Self) -> SeriesStringNamespace[Self]:
         return SeriesStringNamespace(self)
 
     @property
-    def dt(self) -> SeriesDateTimeNamespace:
+    def dt(self: Self) -> SeriesDateTimeNamespace[Self]:
         return SeriesDateTimeNamespace(self)
 
     @property
-    def cat(self) -> SeriesCatNamespace:
+    def cat(self: Self) -> SeriesCatNamespace[Self]:
         return SeriesCatNamespace(self)
 
 
-class SeriesCatNamespace:
-    def __init__(self, series: Series) -> None:
+T = TypeVar("T", bound=Series)
+
+
+class SeriesCatNamespace(Generic[T]):
+    def __init__(self: Self, series: T) -> None:
         self._narwhals_series = series
 
-    def get_categories(self) -> Series:
+    def get_categories(self: Self) -> T:
         """
         Get unique categories from column.
 
@@ -2547,11 +2552,11 @@ class SeriesCatNamespace:
         )
 
 
-class SeriesStringNamespace:
-    def __init__(self, series: Series) -> None:
+class SeriesStringNamespace(Generic[T]):
+    def __init__(self: Self, series: T) -> None:
         self._narwhals_series = series
 
-    def len_chars(self) -> Series:
+    def len_chars(self: Self) -> T:
         r"""
         Return the length of each string as the number of characters.
 
@@ -2595,8 +2600,8 @@ class SeriesStringNamespace:
         )
 
     def replace(
-        self, pattern: str, value: str, *, literal: bool = False, n: int = 1
-    ) -> Series:
+        self: Self, pattern: str, value: str, *, literal: bool = False, n: int = 1
+    ) -> T:
         r"""
         Replace first matching regex/literal substring with a new string value.
 
@@ -2635,7 +2640,7 @@ class SeriesStringNamespace:
             )
         )
 
-    def replace_all(self, pattern: str, value: str, *, literal: bool = False) -> Series:
+    def replace_all(self: Self, pattern: str, value: str, *, literal: bool = False) -> T:
         r"""
         Replace all matching regex/literal substring with a new string value.
 
@@ -2673,7 +2678,7 @@ class SeriesStringNamespace:
             )
         )
 
-    def strip_chars(self, characters: str | None = None) -> Series:
+    def strip_chars(self: Self, characters: str | None = None) -> T:
         r"""
         Remove leading and trailing characters.
 
@@ -2707,7 +2712,7 @@ class SeriesStringNamespace:
             self._narwhals_series._compliant_series.str.strip_chars(characters)
         )
 
-    def starts_with(self, prefix: str) -> Series:
+    def starts_with(self: Self, prefix: str) -> T:
         r"""
         Check if string values start with a substring.
 
@@ -2749,7 +2754,7 @@ class SeriesStringNamespace:
             self._narwhals_series._compliant_series.str.starts_with(prefix)
         )
 
-    def ends_with(self, suffix: str) -> Series:
+    def ends_with(self: Self, suffix: str) -> T:
         r"""
         Check if string values end with a substring.
 
@@ -2791,7 +2796,7 @@ class SeriesStringNamespace:
             self._narwhals_series._compliant_series.str.ends_with(suffix)
         )
 
-    def contains(self, pattern: str, *, literal: bool = False) -> Series:
+    def contains(self: Self, pattern: str, *, literal: bool = False) -> T:
         r"""
         Check if string contains a substring that matches a pattern.
 
@@ -2839,7 +2844,7 @@ class SeriesStringNamespace:
             self._narwhals_series._compliant_series.str.contains(pattern, literal=literal)
         )
 
-    def slice(self, offset: int, length: int | None = None) -> Series:
+    def slice(self: Self, offset: int, length: int | None = None) -> T:
         r"""
         Create subslices of the string values of a Series.
 
@@ -2910,7 +2915,7 @@ class SeriesStringNamespace:
             )
         )
 
-    def head(self, n: int = 5) -> Series:
+    def head(self: Self, n: int = 5) -> T:
         r"""
         Take the first n elements of each string.
 
@@ -2958,7 +2963,7 @@ class SeriesStringNamespace:
             self._narwhals_series._compliant_series.str.slice(0, n)
         )
 
-    def tail(self, n: int = 5) -> Series:
+    def tail(self: Self, n: int = 5) -> T:
         r"""
         Take the last n elements of each string.
 
@@ -3006,7 +3011,7 @@ class SeriesStringNamespace:
             self._narwhals_series._compliant_series.str.slice(-n)
         )
 
-    def to_uppercase(self) -> Series:
+    def to_uppercase(self) -> T:
         r"""
         Transform string to uppercase variant.
 
@@ -3054,7 +3059,7 @@ class SeriesStringNamespace:
             self._narwhals_series._compliant_series.str.to_uppercase()
         )
 
-    def to_lowercase(self) -> Series:
+    def to_lowercase(self) -> T:
         r"""
         Transform string to lowercase variant.
 
@@ -3097,7 +3102,7 @@ class SeriesStringNamespace:
             self._narwhals_series._compliant_series.str.to_lowercase()
         )
 
-    def to_datetime(self: Self, format: str | None = None) -> Series:  # noqa: A002
+    def to_datetime(self: Self, format: str | None = None) -> T:  # noqa: A002
         """
         Parse Series with strings to a Series with Datetime dtype.
 
@@ -3158,11 +3163,11 @@ class SeriesStringNamespace:
         )
 
 
-class SeriesDateTimeNamespace:
-    def __init__(self, series: Series) -> None:
+class SeriesDateTimeNamespace(Generic[T]):
+    def __init__(self: Self, series: T) -> None:
         self._narwhals_series = series
 
-    def date(self) -> Series:
+    def date(self: Self) -> T:
         """
         Get the date in a datetime series.
 
@@ -3203,7 +3208,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.date()
         )
 
-    def year(self) -> Series:
+    def year(self: Self) -> T:
         """
         Get the year in a datetime series.
 
@@ -3240,7 +3245,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.year()
         )
 
-    def month(self) -> Series:
+    def month(self: Self) -> T:
         """
         Gets the month in a datetime series.
 
@@ -3277,7 +3282,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.month()
         )
 
-    def day(self) -> Series:
+    def day(self: Self) -> T:
         """
         Extracts the day in a datetime series.
 
@@ -3314,7 +3319,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.day()
         )
 
-    def hour(self) -> Series:
+    def hour(self: Self) -> T:
         """
          Extracts the hour in a datetime series.
 
@@ -3351,7 +3356,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.hour()
         )
 
-    def minute(self) -> Series:
+    def minute(self: Self) -> T:
         """
         Extracts the minute in a datetime series.
 
@@ -3388,7 +3393,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.minute()
         )
 
-    def second(self) -> Series:
+    def second(self: Self) -> T:
         """
         Extracts the second(s) in a datetime series.
 
@@ -3425,7 +3430,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.second()
         )
 
-    def millisecond(self) -> Series:
+    def millisecond(self: Self) -> T:
         """
         Extracts the milliseconds in a datetime series.
 
@@ -3475,7 +3480,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.millisecond()
         )
 
-    def microsecond(self) -> Series:
+    def microsecond(self: Self) -> T:
         """
         Extracts the microseconds in a datetime series.
 
@@ -3525,7 +3530,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.microsecond()
         )
 
-    def nanosecond(self) -> Series:
+    def nanosecond(self: Self) -> T:
         """
         Extracts the nanosecond(s) in a date series.
 
@@ -3565,7 +3570,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.nanosecond()
         )
 
-    def ordinal_day(self) -> Series:
+    def ordinal_day(self: Self) -> T:
         """
         Get ordinal day.
 
@@ -3602,7 +3607,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.ordinal_day()
         )
 
-    def total_minutes(self) -> Series:
+    def total_minutes(self: Self) -> T:
         """
         Get total minutes.
 
@@ -3644,7 +3649,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.total_minutes()
         )
 
-    def total_seconds(self) -> Series:
+    def total_seconds(self: Self) -> T:
         """
         Get total seconds.
 
@@ -3686,7 +3691,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.total_seconds()
         )
 
-    def total_milliseconds(self) -> Series:
+    def total_milliseconds(self: Self) -> T:
         """
         Get total milliseconds.
 
@@ -3731,7 +3736,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.total_milliseconds()
         )
 
-    def total_microseconds(self) -> Series:
+    def total_microseconds(self: Self) -> T:
         """
         Get total microseconds.
 
@@ -3776,7 +3781,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.total_microseconds()
         )
 
-    def total_nanoseconds(self) -> Series:
+    def total_nanoseconds(self: Self) -> T:
         """
         Get total nanoseconds.
 
@@ -3818,7 +3823,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.total_nanoseconds()
         )
 
-    def to_string(self, format: str) -> Series:  # noqa: A002
+    def to_string(self: Self, format: str) -> T:  # noqa: A002
         """
         Convert a Date/Time/Datetime series into a String series with the given format.
 
@@ -3893,7 +3898,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.to_string(format)
         )
 
-    def replace_time_zone(self, time_zone: str | None) -> Series:
+    def replace_time_zone(self: Self, time_zone: str | None) -> T:
         """
         Replace time zone.
 
@@ -3946,7 +3951,7 @@ class SeriesDateTimeNamespace:
             self._narwhals_series._compliant_series.dt.replace_time_zone(time_zone)
         )
 
-    def convert_time_zone(self, time_zone: str) -> Series:
+    def convert_time_zone(self: Self, time_zone: str) -> T:
         """
         Convert time zone.
 
diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py
index 7bcd6146..f35d9197 100644
--- a/narwhals/stable/v1/__init__.py
+++ b/narwhals/stable/v1/__init__.py
@@ -51,6 +51,7 @@ from narwhals.stable.v1.dtypes import Unknown
 from narwhals.translate import _from_native_impl
 from narwhals.translate import get_native_namespace as nw_get_native_namespace
 from narwhals.translate import to_native
+from narwhals.translate import to_py_scalar as nw_to_py_scalar
 from narwhals.typing import IntoDataFrameT
 from narwhals.typing import IntoFrameT
 from narwhals.typing import IntoSeriesT
@@ -952,6 +953,28 @@ def narwhalify(
         return decorator(func)
 
 
+def to_py_scalar(scalar: Any) -> Any:
+    """If a scalar is not Python native, converts it to Python native.
+
+    Raises:
+        ValueError: If the object is not convertible to a scalar.
+
+    Examples:
+        >>> import narwhals.stable.v1 as nw
+        >>> import pandas as pd
+        >>> df = nw.from_native(pd.DataFrame({"a": [1, 2, 3]}))
+        >>> nw.to_py_scalar(df["a"].item(0))
+        1
+        >>> import pyarrow as pa
+        >>> df = nw.from_native(pa.table({"a": [1, 2, 3]}))
+        >>> nw.to_py_scalar(df["a"].item(0))
+        1
+        >>> nw.to_py_scalar(1)
+        1
+    """
+    return _stableify(nw_to_py_scalar(scalar))
+
+
 def all() -> Expr:
     """
     Instantiate an expression representing all columns.
@@ -959,9 +982,11 @@ def all() -> Expr:
     Examples:
         >>> import polars as pl
         >>> import pandas as pd
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
         >>> df_pl = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+        >>> df_pa = pa.table({"a": [1, 2, 3], "b": [4, 5, 6]})
 
         Let's define a dataframe-agnostic function:
 
@@ -969,7 +994,7 @@ def all() -> Expr:
         ... def func(df):
         ...     return df.select(nw.all() * 2)
 
-        We can then pass either pandas or Polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a   b
@@ -987,6 +1012,13 @@ def all() -> Expr:
         │ 4   ┆ 10  │
         │ 6   ┆ 12  │
         └─────┴─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        b: int64
+        ----
+        a: [[2,4,6]]
+        b: [[8,10,12]]
     """
     return _stableify(nw.all())
 
@@ -1001,9 +1033,11 @@ def col(*names: str | Iterable[str]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [3, 4]})
         >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
+        >>> df_pa = pa.table({"a": [1, 2], "b": [3, 4]})
 
         We define a dataframe-agnostic function:
 
@@ -1011,7 +1045,7 @@ def col(*names: str | Iterable[str]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.col("a") * nw.col("b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a
@@ -1027,6 +1061,11 @@ def col(*names: str | Iterable[str]) -> Expr:
         │ 3   │
         │ 8   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[3,8]]
     """
     return _stableify(nw.col(*names))
 
@@ -1057,7 +1096,7 @@ def nth(*indices: int | Sequence[int]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.nth(0) * 2)
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a
@@ -1089,9 +1128,11 @@ def len() -> Expr:
     Examples:
         >>> import polars as pl
         >>> import pandas as pd
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [5, 10]})
         >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [5, 10]})
+        >>> df_pa = pa.table({"a": [1, 2], "b": [5, 10]})
 
         Let's define a dataframe-agnostic function:
 
@@ -1099,7 +1140,7 @@ def len() -> Expr:
         ... def func(df):
         ...     return df.select(nw.len())
 
-        We can then pass either pandas or Polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            len
@@ -1113,6 +1154,11 @@ def len() -> Expr:
         ╞═════╡
         │ 2   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        len: int64
+        ----
+        len: [[2]]
     """
     return _stableify(nw.len())
 
@@ -1128,9 +1174,11 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> df_pl = pl.DataFrame({"a": [1, 2]})
         >>> df_pd = pd.DataFrame({"a": [1, 2]})
+        >>> df_pa = pa.table({"a": [1, 2]})
 
         We define a dataframe-agnostic function:
 
@@ -1138,7 +1186,7 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
         ... def func(df):
         ...     return df.with_columns(nw.lit(3).alias("b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a  b
@@ -1154,7 +1202,13 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
         │ 1   ┆ 3   │
         │ 2   ┆ 3   │
         └─────┴─────┘
-
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        b: int64
+        ----
+        a: [[1,2]]
+        b: [[3,3]]
     """
     return _stableify(nw.lit(value, dtype))
 
@@ -1172,9 +1226,11 @@ def min(*columns: str) -> Expr:
     Examples:
         >>> import polars as pl
         >>> import pandas as pd
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [5, 10]})
         >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [5, 10]})
+        >>> df_pa = pa.table({"a": [1, 2], "b": [5, 10]})
 
         Let's define a dataframe-agnostic function:
 
@@ -1182,7 +1238,7 @@ def min(*columns: str) -> Expr:
         ... def func(df):
         ...     return df.select(nw.min("b"))
 
-        We can then pass either pandas or Polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            b
@@ -1196,6 +1252,11 @@ def min(*columns: str) -> Expr:
         ╞═════╡
         │ 5   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        b: int64
+        ----
+        b: [[5]]
     """
     return _stableify(nw.min(*columns))
 
@@ -1213,9 +1274,11 @@ def max(*columns: str) -> Expr:
     Examples:
         >>> import polars as pl
         >>> import pandas as pd
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> df_pd = pd.DataFrame({"a": [1, 2], "b": [5, 10]})
         >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [5, 10]})
+        >>> df_pa = pa.table({"a": [1, 2], "b": [5, 10]})
 
         Let's define a dataframe-agnostic function:
 
@@ -1223,7 +1286,7 @@ def max(*columns: str) -> Expr:
         ... def func(df):
         ...     return df.select(nw.max("a"))
 
-        We can then pass either pandas or Polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a
@@ -1237,6 +1300,11 @@ def max(*columns: str) -> Expr:
         ╞═════╡
         │ 2   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[2]]
     """
     return _stableify(nw.max(*columns))
 
@@ -1302,9 +1370,11 @@ def sum(*columns: str) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> df_pl = pl.DataFrame({"a": [1, 2]})
         >>> df_pd = pd.DataFrame({"a": [1, 2]})
+        >>> df_pa = pa.table({"a": [1, 2]})
 
         We define a dataframe-agnostic function:
 
@@ -1312,7 +1382,7 @@ def sum(*columns: str) -> Expr:
         ... def func(df):
         ...     return df.select(nw.sum("a"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a
@@ -1326,6 +1396,11 @@ def sum(*columns: str) -> Expr:
         ╞═════╡
         │ 3   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[3]]
     """
     return _stableify(nw.sum(*columns))
 
@@ -1344,10 +1419,12 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> data = {"a": [1, 2, 3], "b": [5, 10, None]}
         >>> df_pl = pl.DataFrame(data)
         >>> df_pd = pd.DataFrame(data)
+        >>> df_pa = pa.table(data)
 
         We define a dataframe-agnostic function:
 
@@ -1355,7 +1432,7 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.sum_horizontal("a", "b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
               a
@@ -1373,6 +1450,11 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         │ 12  │
         │ 3   │
         └─────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[6,12,3]]
     """
     return _stableify(nw.sum_horizontal(*exprs))
 
@@ -1390,6 +1472,7 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> data = {
         ...     "a": [False, False, True, True, False, None],
@@ -1397,6 +1480,7 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... }
         >>> df_pl = pl.DataFrame(data)
         >>> df_pd = pd.DataFrame(data)
+        >>> df_pa = pa.table(data)
 
         We define a dataframe-agnostic function:
 
@@ -1404,7 +1488,7 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select("a", "b", all=nw.all_horizontal("a", "b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
                a      b    all
@@ -1429,6 +1513,16 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         │ false ┆ null  ┆ false │
         │ null  ┆ null  ┆ null  │
         └───────┴───────┴───────┘
+
+        >>> func(df_pa)
+        pyarrow.Table
+        a: bool
+        b: bool
+        all: bool
+        ----
+        a: [[false,false,true,true,false,null]]
+        b: [[false,true,true,null,null,null]]
+        all: [[false,false,true,null,false,null]]
     """
     return _stableify(nw.all_horizontal(*exprs))
 
@@ -1446,6 +1540,7 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> data = {
         ...     "a": [False, False, True, True, False, None],
@@ -1453,6 +1548,7 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... }
         >>> df_pl = pl.DataFrame(data)
         >>> df_pd = pd.DataFrame(data)
+        >>> df_pa = pa.table(data)
 
         We define a dataframe-agnostic function:
 
@@ -1460,7 +1556,7 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select("a", "b", any=nw.any_horizontal("a", "b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
                a      b    any
@@ -1485,6 +1581,16 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         │ false ┆ null  ┆ null  │
         │ null  ┆ null  ┆ null  │
         └───────┴───────┴───────┘
+
+        >>> func(df_pa)
+        pyarrow.Table
+        a: bool
+        b: bool
+        any: bool
+        ----
+        a: [[false,false,true,true,false,null]]
+        b: [[false,true,true,null,null,null]]
+        any: [[false,true,true,true,null,null]]
     """
     return _stableify(nw.any_horizontal(*exprs))
 
@@ -1500,6 +1606,7 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> data = {
         ...     "a": [1, 8, 3],
@@ -1508,6 +1615,7 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... }
         >>> df_pl = pl.DataFrame(data)
         >>> df_pd = pd.DataFrame(data)
+        >>> df_pa = pa.table(data)
 
         We define a dataframe-agnostic function that computes the horizontal mean of "a"
         and "b" columns:
@@ -1516,13 +1624,14 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.mean_horizontal("a", "b"))
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
              a
         0  2.5
         1  6.5
         2  3.0
+
         >>> func(df_pl)
         shape: (3, 1)
         ┌─────┐
@@ -1534,6 +1643,12 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         │ 6.5 │
         │ 3.0 │
         └─────┘
+
+        >>> func(df_pa)
+        pyarrow.Table
+        a: double
+        ----
+        a: [[2.5,6.5,3]]
     """
     return _stableify(nw.mean_horizontal(*exprs))
 
@@ -1567,7 +1682,7 @@ def min_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.min_horizontal("a", "b"))
 
-        We can then pass either pandas, polars or pyarrow to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(pd.DataFrame(data))
              a
@@ -1623,7 +1738,7 @@ def max_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         ... def func(df):
         ...     return df.select(nw.max_horizontal("a", "b"))
 
-        We can then pass either pandas, polars or pyarrow to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(pd.DataFrame(data))
              a
@@ -1819,7 +1934,7 @@ def concat_str(
         ...         ).alias("full_sentence")
         ...     )
 
-        We can then pass either pandas, Polars or PyArrow to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(pd.DataFrame(data))
           full_sentence
@@ -2098,9 +2213,11 @@ def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When:
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> df_pl = pl.DataFrame({"a": [1, 2, 3], "b": [5, 10, 15]})
         >>> df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [5, 10, 15]})
+        >>> df_pa = pa.table({"a": [1, 2, 3], "b": [5, 10, 15]})
 
         We define a dataframe-agnostic function:
 
@@ -2110,7 +2227,7 @@ def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When:
         ...         nw.when(nw.col("a") < 3).then(5).otherwise(6).alias("a_when")
         ...     )
 
-        We can then pass either pandas or polars to `func`:
+        We can pass any supported library such as Pandas, Polars, or PyArrow to `func`:
 
         >>> func(df_pd)
            a   b  a_when
@@ -2128,6 +2245,15 @@ def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When:
         │ 2   ┆ 10  ┆ 5      │
         │ 3   ┆ 15  ┆ 6      │
         └─────┴─────┴────────┘
+        >>> func(df_pa)
+        pyarrow.Table
+        a: int64
+        b: int64
+        a_when: int64
+        ----
+        a: [[1,2,3]]
+        b: [[5,10,15]]
+        a_when: [[5,5,6]]
     """
     return When.from_when(nw_when(*predicates))
 
@@ -2260,6 +2386,7 @@ def from_dict(
     Examples:
         >>> import pandas as pd
         >>> import polars as pl
+        >>> import pyarrow as pa
         >>> import narwhals.stable.v1 as nw
         >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
 
@@ -2271,7 +2398,7 @@ def from_dict(
         ...     native_namespace = nw.get_native_namespace(df)
         ...     return nw.from_dict(new_data, native_namespace=native_namespace)
 
-        Let's see what happens when passing pandas / Polars input:
+        Let's see what happens when passing Pandas, Polars or PyArrow input:
 
         >>> func(pd.DataFrame(data))
            c  d
@@ -2287,6 +2414,13 @@ def from_dict(
         │ 5   ┆ 1   │
         │ 2   ┆ 4   │
         └─────┴─────┘
+        >>> func(pa.table(data))
+        pyarrow.Table
+        c: int64
+        d: int64
+        ----
+        c: [[5,2]]
+        d: [[1,4]]
     """
     from narwhals.stable.v1 import dtypes
 
@@ -2306,6 +2440,7 @@ __all__ = [
     "dependencies",
     "to_native",
     "from_native",
+    "to_py_scalar",
     "is_ordered_categorical",
     "maybe_align_index",
     "maybe_convert_dtypes",
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 0dc0cd46..331b87d8 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -1,5 +1,8 @@
 from __future__ import annotations
 
+import numbers
+from datetime import datetime
+from datetime import timedelta
 from functools import wraps
 from typing import TYPE_CHECKING
 from typing import Any
@@ -9,9 +12,11 @@ from typing import TypeVar
 from typing import overload
 
 from narwhals.dependencies import get_cudf
+from narwhals.dependencies import get_cupy
 from narwhals.dependencies import get_dask
 from narwhals.dependencies import get_dask_expr
 from narwhals.dependencies import get_modin
+from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
 from narwhals.dependencies import get_polars
 from narwhals.dependencies import get_pyarrow
@@ -776,8 +781,70 @@ def narwhalify(
         return decorator(func)
 
 
+def to_py_scalar(scalar_like: Any) -> Any:
+    """If a scalar is not Python native, converts it to Python native.
+
+    Raises:
+        ValueError: If the object is not convertible to a scalar.
+
+    Examples:
+        >>> import narwhals as nw
+        >>> import pandas as pd
+        >>> df = nw.from_native(pd.DataFrame({"a": [1, 2, 3]}))
+        >>> nw.to_py_scalar(df["a"].item(0))
+        1
+        >>> import pyarrow as pa
+        >>> df = nw.from_native(pa.table({"a": [1, 2, 3]}))
+        >>> nw.to_py_scalar(df["a"].item(0))
+        1
+        >>> nw.to_py_scalar(1)
+        1
+    """
+
+    pa = get_pyarrow()
+    if pa and isinstance(scalar_like, pa.Scalar):
+        return scalar_like.as_py()
+
+    cupy = get_cupy()
+    if (  # pragma: no cover
+        cupy and isinstance(scalar_like, cupy.ndarray) and scalar_like.size == 1
+    ):
+        return scalar_like.item()
+
+    np = get_numpy()
+    if np and np.isscalar(scalar_like) and hasattr(scalar_like, "item"):
+        return scalar_like.item()
+
+    pd = get_pandas()
+    if pd and isinstance(scalar_like, pd.Timestamp):
+        return scalar_like.to_pydatetime()
+    if pd and isinstance(scalar_like, pd.Timedelta):
+        return scalar_like.to_pytimedelta()
+
+    all_scalar_types = (
+        int,
+        float,
+        complex,
+        bool,
+        bytes,
+        str,
+        datetime,
+        timedelta,
+        numbers.Number,
+    )
+    if isinstance(scalar_like, all_scalar_types):
+        return scalar_like
+
+    msg = (
+        f"Expected object convertible to a scalar, found {type(scalar_like)}. "
+        "Please report a bug to https://github.com/narwhals-dev/narwhals/issues"
+    )
+    raise ValueError(msg)
+
+
 __all__ = [
     "get_native_namespace",
     "to_native",
     "narwhalify",
+    "to_py_scalar",
 ]
diff --git a/noxfile.py b/noxfile.py
index 1fb820c6..aec70add 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -27,7 +27,9 @@ def run_common(session: Session, coverage_threshold: float) -> None:
         f"--cov-fail-under={coverage_threshold}",
         "--runslow",
     )
-    session.run("pytest", "narwhals", "--doctest-modules")
+
+    if session.python == "3.12":
+        session.run("pytest", "narwhals", "--doctest-modules")
 
 
 @nox.session(python=PYTHON_VERSIONS)  # type: ignore[misc]


Test patch:
diff --git a/tests/expr_and_series/str/contains_test.py b/tests/expr_and_series/str/contains_test.py
index 2c2e0cb9..98e8ceaa 100644
--- a/tests/expr_and_series/str/contains_test.py
+++ b/tests/expr_and_series/str/contains_test.py
@@ -1,7 +1,5 @@
 from __future__ import annotations
 
-import pandas as pd
-import polars as pl
 import pytest
 
 import narwhals.stable.v1 as nw
@@ -11,9 +9,6 @@ from tests.utils import compare_dicts
 
 data = {"pets": ["cat", "dog", "rabbit and parrot", "dove"]}
 
-df_pandas = pd.DataFrame(data)
-df_polars = pl.DataFrame(data)
-
 
 def test_contains_case_insensitive(
     constructor: Constructor, request: pytest.FixtureRequest
diff --git a/tests/series_only/cast_test.py b/tests/series_only/cast_test.py
index 55752149..c5b37b8c 100644
--- a/tests/series_only/cast_test.py
+++ b/tests/series_only/cast_test.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from datetime import date
 from datetime import datetime
+from typing import TYPE_CHECKING
 
 import pandas as pd
 import polars as pl
@@ -12,16 +13,18 @@ from polars.testing import assert_frame_equal
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
 
+if TYPE_CHECKING:
+    from tests.utils import ConstructorEager
 
-def test_cast_253() -> None:
-    df_polars = pl.DataFrame({"a": [1]})
-    result = nw.from_native(df_polars, eager_only=True).select(
-        nw.col("a").cast(nw.String) + "hi"
-    )["a"][0]
-    assert result == "1hi"
 
-    df_pandas = pd.DataFrame({"a": [1]})
-    result = nw.from_native(df_pandas, eager_only=True).select(
+def test_cast_253(
+    request: pytest.FixtureRequest, constructor_eager: ConstructorEager
+) -> None:
+    if "pyarrow_table" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
+
+    df_raw = constructor_eager({"a": [1]})
+    result = nw.from_native(df_raw, eager_only=True).select(
         nw.col("a").cast(nw.String) + "hi"
     )["a"][0]
     assert result == "1hi"
diff --git a/tests/translate/to_py_scalar_test.py b/tests/translate/to_py_scalar_test.py
new file mode 100644
index 00000000..c9aa2749
--- /dev/null
+++ b/tests/translate/to_py_scalar_test.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+from datetime import datetime
+from datetime import timedelta
+from typing import TYPE_CHECKING
+from typing import Any
+
+import numpy as np
+import pandas as pd
+import pytest
+
+import narwhals.stable.v1 as nw
+from narwhals.dependencies import get_cudf
+
+if TYPE_CHECKING:
+    from tests.utils import ConstructorEager
+
+
+@pytest.mark.parametrize(
+    ("input_value", "expected"),
+    [
+        (1, 1),
+        (1.0, 1.0),
+        ("a", "a"),
+        (True, True),
+        (b"a", b"a"),
+        (datetime(2021, 1, 1), datetime(2021, 1, 1)),
+        (timedelta(days=1), timedelta(days=1)),
+    ],
+)
+def test_to_py_scalar(
+    constructor_eager: ConstructorEager, input_value: Any, expected: Any
+) -> None:
+    df = nw.from_native(constructor_eager({"a": [input_value]}))
+    output = nw.to_py_scalar(df["a"].item(0))
+    if expected == 1 and constructor_eager.__name__.startswith("pandas"):
+        assert not isinstance(output, np.int64)
+    elif isinstance(expected, datetime) and constructor_eager.__name__.startswith(
+        "pandas"
+    ):
+        assert not isinstance(output, pd.Timestamp)
+    elif isinstance(expected, timedelta) and constructor_eager.__name__.startswith(
+        "pandas"
+    ):
+        assert not isinstance(output, pd.Timedelta)
+    assert output == expected
+
+
+@pytest.mark.parametrize(
+    "input_value",
+    [np.array([1, 2]), [1, 2, 3], {"a": [1, 2, 3]}],
+)
+def test_to_py_scalar_value_error(input_value: Any) -> None:
+    with pytest.raises(ValueError, match="Expected object convertible to a scalar"):
+        nw.to_py_scalar(input_value)
+
+
+def test_to_py_scalar_value_error_cudf() -> None:
+    if cudf := get_cudf():  # pragma: no cover
+        df = nw.from_native(cudf.DataFrame({"a": [1, 2, 3]}))
+
+        with pytest.raises(ValueError, match="Expected object convertible to a scalar"):
+            nw.to_py_scalar(df["a"])