Repository: narwhals-dev/narwhals
Pull request #544 (2024-07-17 06:00:48)

feat: add `__floordiv__` and `__rfloordiv__` for pyarrow
<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue #543 #372  #491 
- Closes #

## Checklist

- [x] Code follows style guide (ruff)
- [ ] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.
PR splitted to give more space for truediv

MarcoGorelli: thanks @lucianosrp 

I think this may need adjusting for negative numbers:
```

In [6]: pl.Series([-100]) // pl.Series([48])
Out[6]:
shape: (1,)
Series: '' [i64]
[
        -3
]

In [7]: pc.divide(-100, 48)
Out[7]: <pyarrow.Int64Scalar: -2>
```
MarcoGorelli: could just copy (with a link) what pandas does? https://github.com/pandas-dev/pandas/blob/262fcfbffcee5c3116e86a951d8b693f90411e68/pandas/core/arrays/arrow/array.py#L124-L154
lucianosrp: @MarcoGorelli , I think I did a bit of a mess, let me know if this looks fine 😅

MarcoGorelli: thanks @lucianosrp ! the code looks good, it just looks like it fails on old versions of pyarrow

I think all we need to do is, in `__floordiv__`, to do a check on `self._backend_version`, and if it's below a certain version (to be determined), then we raise `"__floordiv__ requires at least pyarrow version ..."`?

Patch:
diff --git a/docs/related.md b/docs/related.md
index 5c35db5b..682a18d4 100644
--- a/docs/related.md
+++ b/docs/related.md
@@ -5,34 +5,23 @@
 Standardised way of interchanging data between libraries, see
 [here](https://data-apis.org/dataframe-protocol/latest/index.html).
 
+Narwhals builds upon it by providing one level of support to libraries which implement it -
+this includes Ibis and Vaex. See [levels](levels.md) for details.
+
 ## DataFrame API Standard
 
 Now-discontinued project which aimed to "provide a standard interface that encapsulates implementation details of dataframe libraries. This will allow users and third-party libraries to write code that interacts and operates with a standard dataframe, and not with specific implementations.", see [here](https://data-apis.org/dataframe-api/draft/).
 
-The Narwhals author was originally involved, but left due to irreconcilable differences in vision.
+The Narwhals author was originally involved, but left due to irreconcilable differences in vision, and
+the project was ultimately abandoned.
 
 Some notable difference are:
 
 - Narwhals just uses a subset of the Polars API, whereas the dataframe standard introduces a new API
 - Narwhals supports expressions, and separates lazy and eager execution
-- Narwhals is a standalone, independent project, whereas the dataframe standard aims to be upstreamed
+- Narwhals is a standalone, independent project, whereas the dataframe standard aimed to be upstreamed
   and implemented by major dataframe libraries.
 
-## Ibis
-
-[Presents itself as a dataframe standard](https://voltrondata.com/resources/open-source-standards), and
-dispatches to 20+ backends. Some differences with Narwhals are:
-
-- Narwhals is aimed at library maintainers, Ibis more to end users
-- Narwhals has zero required dependencies, whereas Ibis requires pandas and PyArrow. For users starting
-  from non-pandas environments, the difference in the relative size increase is ~1000x
-- Narwhals only supports 4 backends, Ibis more than 20
-- Narwhals is focused on fundamental dataframe operations, Ibis on SQL backends
-- Narwhals has negligible overhead for dataframe backends, while
-  [Ibis' overhead can be significant](https://github.com/ibis-project/ibis/issues/9345)
-
-The projects are not in competition and have different goals.
-
 ## Array API
 
 Array counterpart to the DataFrame API, see [here](https://data-apis.org/array-api/2022.12/index.html).
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 94dccd0c..7526e528 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -14,6 +14,7 @@ from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pyarrow
 from narwhals.dependencies import get_pyarrow_parquet
 from narwhals.utils import flatten
+from narwhals.utils import generate_unique_token
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -208,7 +209,7 @@ class ArrowDataFrame:
         self,
         other: Self,
         *,
-        how: Literal["inner"] = "inner",
+        how: Literal["left", "inner", "outer", "cross", "anti", "semi"] = "inner",
         left_on: str | list[str] | None,
         right_on: str | list[str] | None,
     ) -> Self:
@@ -217,24 +218,35 @@ class ArrowDataFrame:
         if isinstance(right_on, str):
             right_on = [right_on]
 
-        if how == "cross":  # type: ignore[comparison-overlap]
-            raise NotImplementedError
-
-        if how == "anti":  # type: ignore[comparison-overlap]
-            raise NotImplementedError
+        how_to_join_map = {
+            "anti": "left anti",
+            "semi": "left semi",
+            "inner": "inner",
+            "left": "left outer",
+        }
 
-        if how == "semi":  # type: ignore[comparison-overlap]
-            raise NotImplementedError
+        if how == "cross":
+            plx = self.__narwhals_namespace__()
+            key_token = generate_unique_token(
+                n_bytes=8, columns=[*self.columns, *other.columns]
+            )
 
-        if how == "left":  # type: ignore[comparison-overlap]
-            raise NotImplementedError
+            return self._from_native_dataframe(
+                self.with_columns(**{key_token: plx.lit(0, None)})._native_dataframe.join(
+                    other.with_columns(**{key_token: plx.lit(0, None)})._native_dataframe,
+                    keys=key_token,
+                    right_keys=key_token,
+                    join_type="inner",
+                    right_suffix="_right",
+                ),
+            ).drop(key_token)
 
         return self._from_native_dataframe(
             self._native_dataframe.join(
                 other._native_dataframe,
                 keys=left_on,
                 right_keys=right_on,
-                join_type=how,
+                join_type=how_to_join_map[how],
                 right_suffix="_right",
             ),
         )
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index d3e9ab05..67ff33de 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -125,6 +125,12 @@ class ArrowExpr:
     def __rpow__(self, other: ArrowExpr | Any) -> Self:
         return reuse_series_implementation(self, "__rpow__", other)
 
+    def __floordiv__(self, other: ArrowExpr | Any) -> Self:
+        return reuse_series_implementation(self, "__floordiv__", other)
+
+    def __rfloordiv__(self, other: ArrowExpr | Any) -> Self:
+        return reuse_series_implementation(self, "__rfloordiv__", other)
+
     def __invert__(self) -> Self:
         return reuse_series_implementation(self, "__invert__")
 
@@ -215,6 +221,9 @@ class ArrowExpr:
             self, "sample", n=n, fraction=fraction, with_replacement=with_replacement
         )
 
+    def fill_null(self: Self, value: Any) -> Self:
+        return reuse_series_implementation(self, "fill_null", value=value)
+
     @property
     def dt(self) -> ArrowExprDateTimeNamespace:
         return ArrowExprDateTimeNamespace(self)
@@ -254,6 +263,40 @@ class ArrowExprStringNamespace:
     def __init__(self, expr: ArrowExpr) -> None:
         self._expr = expr
 
+    def starts_with(self, prefix: str) -> ArrowExpr:
+        return reuse_series_namespace_implementation(
+            self._expr,
+            "str",
+            "starts_with",
+            prefix,
+        )
+
+    def ends_with(self, suffix: str) -> ArrowExpr:
+        return reuse_series_namespace_implementation(
+            self._expr,
+            "str",
+            "ends_with",
+            suffix,
+        )
+
+    def contains(self, pattern: str, *, literal: bool) -> ArrowExpr:
+        return reuse_series_namespace_implementation(
+            self._expr, "str", "contains", pattern, literal=literal
+        )
+
+    def slice(self, offset: int, length: int | None = None) -> ArrowExpr:
+        return reuse_series_namespace_implementation(
+            self._expr, "str", "slice", offset, length
+        )
+
+    def to_datetime(self, format: str | None = None) -> ArrowExpr:  # noqa: A002
+        return reuse_series_namespace_implementation(
+            self._expr,
+            "str",
+            "to_datetime",
+            format,
+        )
+
     def to_uppercase(self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index 40e1d1b7..bc045565 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -8,6 +8,7 @@ from typing import Iterable
 from narwhals import dtypes
 from narwhals._arrow.dataframe import ArrowDataFrame
 from narwhals._arrow.expr import ArrowExpr
+from narwhals._arrow.selectors import ArrowSelectorNamespace
 from narwhals._arrow.series import ArrowSeries
 from narwhals._arrow.utils import horizontal_concat
 from narwhals._arrow.utils import vertical_concat
@@ -198,3 +199,7 @@ class ArrowNamespace:
                 backend_version=self._backend_version,
             )
         raise NotImplementedError
+
+    @property
+    def selectors(self) -> ArrowSelectorNamespace:
+        return ArrowSelectorNamespace(backend_version=self._backend_version)
diff --git a/narwhals/_arrow/selectors.py b/narwhals/_arrow/selectors.py
new file mode 100644
index 00000000..b8ba5bf5
--- /dev/null
+++ b/narwhals/_arrow/selectors.py
@@ -0,0 +1,161 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing import Any
+from typing import NoReturn
+
+from narwhals import dtypes
+from narwhals._arrow.expr import ArrowExpr
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+    from narwhals._arrow.dataframe import ArrowDataFrame
+    from narwhals._arrow.series import ArrowSeries
+    from narwhals.dtypes import DType
+
+
+class ArrowSelectorNamespace:
+    def __init__(self: Self, *, backend_version: tuple[int, ...]) -> None:
+        self._backend_version = backend_version
+
+    def by_dtype(self: Self, dtypes: list[DType | type[DType]]) -> ArrowSelector:
+        def func(df: ArrowDataFrame) -> list[ArrowSeries]:
+            return [df[col] for col in df.columns if df.schema[col] in dtypes]
+
+        return ArrowSelector(
+            func,
+            depth=0,
+            function_name="type_selector",
+            root_names=None,
+            output_names=None,
+            backend_version=self._backend_version,
+        )
+
+    def numeric(self: Self) -> ArrowSelector:
+        return self.by_dtype(
+            [
+                dtypes.Int64,
+                dtypes.Int32,
+                dtypes.Int16,
+                dtypes.Int8,
+                dtypes.UInt64,
+                dtypes.UInt32,
+                dtypes.UInt16,
+                dtypes.UInt8,
+                dtypes.Float64,
+                dtypes.Float32,
+            ],
+        )
+
+    def categorical(self: Self) -> ArrowSelector:
+        return self.by_dtype([dtypes.Categorical])
+
+    def string(self: Self) -> ArrowSelector:
+        return self.by_dtype([dtypes.String])
+
+    def boolean(self: Self) -> ArrowSelector:
+        return self.by_dtype([dtypes.Boolean])
+
+    def all(self: Self) -> ArrowSelector:
+        def func(df: ArrowDataFrame) -> list[ArrowSeries]:
+            return [df[col] for col in df.columns]
+
+        return ArrowSelector(
+            func,
+            depth=0,
+            function_name="type_selector",
+            root_names=None,
+            output_names=None,
+            backend_version=self._backend_version,
+        )
+
+
+class ArrowSelector(ArrowExpr):
+    def __repr__(self: Self) -> str:  # pragma: no cover
+        return (
+            f"ArrowSelector("
+            f"depth={self._depth}, "
+            f"function_name={self._function_name}, "
+            f"root_names={self._root_names}, "
+            f"output_names={self._output_names}"
+        )
+
+    def _to_expr(self: Self) -> ArrowExpr:
+        return ArrowExpr(
+            self._call,
+            depth=self._depth,
+            function_name=self._function_name,
+            root_names=self._root_names,
+            output_names=self._output_names,
+            backend_version=self._backend_version,
+        )
+
+    def __sub__(self: Self, other: Self | Any) -> ArrowSelector | Any:
+        if isinstance(other, ArrowSelector):
+
+            def call(df: ArrowDataFrame) -> list[ArrowSeries]:
+                lhs = self._call(df)
+                rhs = other._call(df)
+                return [x for x in lhs if x.name not in [x.name for x in rhs]]
+
+            return ArrowSelector(
+                call,
+                depth=0,
+                function_name="type_selector",
+                root_names=None,
+                output_names=None,
+                backend_version=self._backend_version,
+            )
+        else:
+            return self._to_expr() - other
+
+    def __or__(self: Self, other: Self | Any) -> ArrowSelector | Any:
+        if isinstance(other, ArrowSelector):
+
+            def call(df: ArrowDataFrame) -> list[ArrowSeries]:
+                lhs = self._call(df)
+                rhs = other._call(df)
+                return [x for x in lhs if x.name not in [x.name for x in rhs]] + rhs
+
+            return ArrowSelector(
+                call,
+                depth=0,
+                function_name="type_selector",
+                root_names=None,
+                output_names=None,
+                backend_version=self._backend_version,
+            )
+        else:
+            return self._to_expr() | other
+
+    def __and__(self: Self, other: Self | Any) -> ArrowSelector | Any:
+        if isinstance(other, ArrowSelector):
+
+            def call(df: ArrowDataFrame) -> list[ArrowSeries]:
+                lhs = self._call(df)
+                rhs = other._call(df)
+                return [x for x in lhs if x.name in [x.name for x in rhs]]
+
+            return ArrowSelector(
+                call,
+                depth=0,
+                function_name="type_selector",
+                root_names=None,
+                output_names=None,
+                backend_version=self._backend_version,
+            )
+        else:
+            return self._to_expr() & other
+
+    def __invert__(self: Self) -> ArrowSelector:
+        return ArrowSelectorNamespace(backend_version=self._backend_version).all() - self
+
+    def __rsub__(self: Self, other: Any) -> NoReturn:
+        raise NotImplementedError
+
+    def __rand__(self: Self, other: Any) -> NoReturn:
+        raise NotImplementedError
+
+    def __ror__(self: Self, other: Any) -> NoReturn:
+        raise NotImplementedError
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 3e87d9ef..cb645f3d 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -5,6 +5,7 @@ from typing import Any
 from typing import Iterable
 from typing import Sequence
 
+from narwhals._arrow.utils import floordiv_compat
 from narwhals._arrow.utils import reverse_translate_dtype
 from narwhals._arrow.utils import translate_dtype
 from narwhals._arrow.utils import validate_column_comparand
@@ -140,6 +141,16 @@ class ArrowSeries:
         other = validate_column_comparand(other)
         return self._from_native_series(pc.power(other, ser))
 
+    def __floordiv__(self, other: Any) -> Self:
+        ser = self._native_series
+        other = validate_column_comparand(other)
+        return self._from_native_series(floordiv_compat(ser, other))
+
+    def __rfloordiv__(self, other: Any) -> Self:
+        ser = self._native_series
+        other = validate_column_comparand(other)
+        return self._from_native_series(floordiv_compat(other, ser))
+
     def __invert__(self) -> Self:
         pc = get_pyarrow_compute()
         return self._from_native_series(pc.invert(self._native_series))
@@ -320,6 +331,14 @@ class ArrowSeries:
         mask = np.random.choice(idx, size=n, replace=with_replacement)
         return self._from_native_series(pc.take(ser, mask))
 
+    def fill_null(self: Self, value: Any) -> Self:
+        pa = get_pyarrow()
+        pc = get_pyarrow_compute()
+        ser = self._native_series
+        dtype = ser.type
+
+        return self._from_native_series(pc.fill_null(ser, pa.scalar(value, dtype)))
+
     @property
     def shape(self) -> tuple[int]:
         return (len(self._native_series),)
@@ -367,16 +386,50 @@ class ArrowSeriesCatNamespace:
 
 
 class ArrowSeriesStringNamespace:
-    def __init__(self, series: ArrowSeries) -> None:
+    def __init__(self: Self, series: ArrowSeries) -> None:
         self._arrow_series = series
 
-    def to_uppercase(self) -> ArrowSeries:
+    def starts_with(self: Self, prefix: str) -> ArrowSeries:
+        pc = get_pyarrow_compute()
+        return self._arrow_series._from_native_series(
+            pc.equal(self.slice(0, len(prefix))._native_series, prefix)
+        )
+
+    def ends_with(self: Self, suffix: str) -> ArrowSeries:
+        pc = get_pyarrow_compute()
+        return self._arrow_series._from_native_series(
+            pc.equal(self.slice(-len(suffix))._native_series, suffix)
+        )
+
+    def contains(self: Self, pattern: str, *, literal: bool = False) -> ArrowSeries:
+        pc = get_pyarrow_compute()
+        check_func = pc.match_substring if literal else pc.match_substring_regex
+        return self._arrow_series._from_native_series(
+            check_func(self._arrow_series._native_series, pattern)
+        )
+
+    def slice(self: Self, offset: int, length: int | None = None) -> ArrowSeries:
+        pc = get_pyarrow_compute()
+        stop = offset + length if length else None
+        return self._arrow_series._from_native_series(
+            pc.utf8_slice_codeunits(
+                self._arrow_series._native_series, start=offset, stop=stop
+            ),
+        )
+
+    def to_datetime(self: Self, format: str | None = None) -> ArrowSeries:  # noqa: A002
+        pc = get_pyarrow_compute()
+        return self._arrow_series._from_native_series(
+            pc.strptime(self._arrow_series._native_series, format=format, unit="us")
+        )
+
+    def to_uppercase(self: Self) -> ArrowSeries:
         pc = get_pyarrow_compute()
         return self._arrow_series._from_native_series(
             pc.utf8_upper(self._arrow_series._native_series),
         )
 
-    def to_lowercase(self) -> ArrowSeries:
+    def to_lowercase(self: Self) -> ArrowSeries:
         pc = get_pyarrow_compute()
         return self._arrow_series._from_native_series(
             pc.utf8_lower(self._arrow_series._native_series),
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index 13fea297..9ab594d9 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -4,6 +4,7 @@ from typing import Any
 
 from narwhals import dtypes
 from narwhals.dependencies import get_pyarrow
+from narwhals.dependencies import get_pyarrow_compute
 from narwhals.utils import isinstance_or_issubclass
 
 
@@ -183,3 +184,42 @@ def vertical_concat(dfs: list[Any]) -> Any:
 
     pa = get_pyarrow()
     return pa.concat_tables(dfs).combine_chunks()
+
+
+def floordiv_compat(left: Any, right: Any) -> Any:
+    # The following lines are adapted from pandas' pyarrow implementation.
+    # Ref: https://github.com/pandas-dev/pandas/blob/262fcfbffcee5c3116e86a951d8b693f90411e68/pandas/core/arrays/arrow/array.py#L124-L154
+    pc = get_pyarrow_compute()
+    pa = get_pyarrow()
+
+    if isinstance(left, (int, float)):
+        left = pa.scalar(left)
+
+    if isinstance(right, (int, float)):
+        right = pa.scalar(right)
+
+    if pa.types.is_integer(left.type) and pa.types.is_integer(right.type):
+        divided = pc.divide_checked(left, right)
+        if pa.types.is_signed_integer(divided.type):
+            # GH 56676
+            has_remainder = pc.not_equal(pc.multiply(divided, right), left)
+            has_one_negative_operand = pc.less(
+                pc.bit_wise_xor(left, right),
+                pa.scalar(0, type=divided.type),
+            )
+            result = pc.if_else(
+                pc.and_(
+                    has_remainder,
+                    has_one_negative_operand,
+                ),
+                # GH: 55561 ruff: ignore
+                pc.subtract(divided, pa.scalar(1, type=divided.type)),
+                divided,
+            )
+        else:
+            result = divided  # pragma: no cover
+        result = result.cast(left.type)
+    else:
+        divided = pc.divide(left, right)
+        result = pc.floor(divided)
+    return result
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index fa6e707c..6606e8f1 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -13,7 +13,6 @@ from narwhals._expression_parsing import evaluate_into_exprs
 from narwhals._pandas_like.expr import PandasLikeExpr
 from narwhals._pandas_like.utils import Implementation
 from narwhals._pandas_like.utils import create_native_series
-from narwhals._pandas_like.utils import generate_unique_token
 from narwhals._pandas_like.utils import horizontal_concat
 from narwhals._pandas_like.utils import translate_dtype
 from narwhals._pandas_like.utils import validate_dataframe_comparand
@@ -23,6 +22,7 @@ from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
 from narwhals.utils import flatten
+from narwhals.utils import generate_unique_token
 
 if TYPE_CHECKING:
     from typing_extensions import Self
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 3548966c..b493cfed 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import secrets
 from enum import Enum
 from enum import auto
 from typing import TYPE_CHECKING
@@ -458,31 +457,3 @@ def int_dtype_mapper(dtype: Any) -> str:
     if str(dtype).lower() != str(dtype):  # pragma: no cover
         return "Int64"
     return "int64"
-
-
-def generate_unique_token(n_bytes: int, columns: list[str]) -> str:  # pragma: no cover
-    """Generates a unique token of specified n_bytes that is not present in the given list of columns.
-
-    Arguments:
-        n_bytes : The number of bytes to generate for the token.
-        columns : The list of columns to check for uniqueness.
-
-    Returns:
-        A unique token that is not present in the given list of columns.
-
-    Raises:
-        AssertionError: If a unique token cannot be generated after 100 attempts.
-    """
-    counter = 0
-    while True:
-        token = secrets.token_hex(n_bytes)
-        if token not in columns:
-            return token
-
-        counter += 1
-        if counter > 100:
-            msg = (
-                "Internal Error: Narwhals was not able to generate a column name to perform cross "
-                "join operation"
-            )
-            raise AssertionError(msg)
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index fb690f49..f4aa363d 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -280,10 +280,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.to_pandas()
-            ...     return df
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.to_pandas()
 
             We can then pass either pandas or Polars to `func`:
 
@@ -343,10 +342,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.to_numpy()
-            ...     return df
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.to_numpy()
 
             We can then pass either pandas or Polars to `func`:
 
@@ -378,9 +376,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     return df.shape
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.shape
 
             We can then pass either pandas or Polars to `func`:
 
@@ -413,9 +411,9 @@ class DataFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify(eager_only=True)
-            ... def func(df):
-            ...     name = df.columns[0]
-            ...     return df.get_column(name)
+            ... def func(df_any):
+            ...     name = df_any.columns[0]
+            ...     return df_any.get_column(name)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -481,8 +479,8 @@ class DataFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify(eager_only=True)
-            ... def func(df):
-            ...     return df["a"]
+            ... def func(df_any):
+            ...     return df_any["a"]
 
             We can then pass either pandas or Polars to `func`:
 
@@ -549,10 +547,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.to_dict(as_series=False)
-            ...     return df
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.to_dict(as_series=False)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -590,12 +587,11 @@ class DataFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.pipe(
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.pipe(
             ...         lambda _df: _df.select([x for x in _df.columns if len(x) == 1])
             ...     )
-            ...     return nw.to_native(df)
 
             We can then pass either pandas or Polars:
 
@@ -636,10 +632,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.drop_nulls()
-            ...     return nw.to_native(df)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.drop_nulls()
 
             We can then pass either pandas or Polars:
 
@@ -674,10 +669,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.with_row_index()
-            ...     return nw.to_native(df)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.with_row_index()
 
             We can then pass either pandas or Polars:
 
@@ -719,9 +713,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     return df.schema
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.schema
 
             You can pass either pandas or Polars to `func`:
 
@@ -753,9 +747,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     return df.collect_schema()
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.collect_schema()
 
             You can pass either pandas or Polars to `func`:
 
@@ -785,8 +779,8 @@ class DataFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.columns
+            ... def func(df_any):
+            ...     return df_any.columns
 
             We can then pass either pandas or Polars to `func`:
 
@@ -841,9 +835,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any, *, named):
-            ...     df = nw.from_native(df_any)
-            ...     return df.rows(named=named)
+            >>> @nw.narwhalify
+            ... def func(df_any, *, named):
+            ...     return df_any.rows(named=named)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -897,9 +891,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any, *, named):
-            ...     df = nw.from_native(df_any)
-            ...     return df.iter_rows(named=named)
+            >>> @nw.narwhalify
+            ... def func(df_any, *, named):
+            ...     return df_any.iter_rows(named=named)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -953,8 +947,8 @@ class DataFrame(BaseFrame[FrameT]):
             to add it as a new column:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns((nw.col("a") * 2).alias("a*2"))
+            ... def func(df_any):
+            ...     return df_any.with_columns((nw.col("a") * 2).alias("a*2"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1011,8 +1005,8 @@ class DataFrame(BaseFrame[FrameT]):
             column to select that column.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select("foo")
+            ... def func(df_any):
+            ...     return df_any.select("foo")
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1036,8 +1030,8 @@ class DataFrame(BaseFrame[FrameT]):
             Multiple columns can be selected by passing a list of column names.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(["foo", "bar"])
+            ... def func(df_any):
+            ...     return df_any.select(["foo", "bar"])
             >>> func(df_pd)
                foo  bar
             0    1    6
@@ -1059,8 +1053,8 @@ class DataFrame(BaseFrame[FrameT]):
             list. Expressions are also accepted.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("foo"), nw.col("bar") + 1)
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("foo"), nw.col("bar") + 1)
             >>> func(df_pd)
                foo  bar
             0    1    7
@@ -1081,8 +1075,8 @@ class DataFrame(BaseFrame[FrameT]):
             Use keyword arguments to easily name your expression inputs.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(threshold=nw.col("foo") * 2)
+            ... def func(df_any):
+            ...     return df_any.select(threshold=nw.col("foo") * 2)
             >>> func(df_pd)
                threshold
             0          2
@@ -1120,8 +1114,8 @@ class DataFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.rename({"foo": "apple"})
+            ... def func(df_any):
+            ...     return df_any.rename({"foo": "apple"})
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1167,8 +1161,8 @@ class DataFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function that gets the first 3 rows.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.head(3)
+            ... def func(df_any):
+            ...     return df_any.head(3)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1215,8 +1209,8 @@ class DataFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function that gets the last 3 rows.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.tail(3)
+            ... def func(df_any):
+            ...     return df_any.tail(3)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1257,8 +1251,8 @@ class DataFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.drop("ham")
+            ... def func(df_any):
+            ...     return df_any.drop("ham")
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1282,8 +1276,8 @@ class DataFrame(BaseFrame[FrameT]):
             Use positional arguments to drop multiple columns.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.drop("foo", "ham")
+            ... def func(df_any):
+            ...     return df_any.drop("foo", "ham")
 
             >>> func(df_pd)
                bar
@@ -1326,8 +1320,8 @@ class DataFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.unique(["bar", "ham"])
+            ... def func(df_any):
+            ...     return df_any.unique(["bar", "ham"])
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1371,8 +1365,8 @@ class DataFrame(BaseFrame[FrameT]):
             one condition.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.filter(nw.col("foo") > 1)
+            ... def func(df_any):
+            ...     return df_any.filter(nw.col("foo") > 1)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1394,8 +1388,8 @@ class DataFrame(BaseFrame[FrameT]):
             Filter on multiple conditions, combined with and/or operators:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.filter((nw.col("foo") < 3) & (nw.col("ham") == "a"))
+            ... def func(df_any):
+            ...     return df_any.filter((nw.col("foo") < 3) & (nw.col("ham") == "a"))
             >>> func(df_pd)
                foo  bar ham
             0    1    6   a
@@ -1410,8 +1404,8 @@ class DataFrame(BaseFrame[FrameT]):
             └─────┴─────┴─────┘
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.filter((nw.col("foo") == 1) | (nw.col("ham") == "c"))
+            ... def func(df_any):
+            ...     return df_any.filter((nw.col("foo") == 1) | (nw.col("ham") == "c"))
             >>> func(df_pd)
                foo  bar ham
             0    1    6   a
@@ -1430,8 +1424,8 @@ class DataFrame(BaseFrame[FrameT]):
             Provide multiple filters using `*args` syntax:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     dframe = df.filter(
+            ... def func(df_any):
+            ...     dframe = df_any.filter(
             ...         nw.col("foo") <= 2,
             ...         ~nw.col("ham").is_in(["b", "c"]),
             ...     )
@@ -1477,8 +1471,8 @@ class DataFrame(BaseFrame[FrameT]):
             and call `agg` to compute the grouped sum of another column.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.group_by("a").agg(nw.col("b").sum()).sort("a")
+            ... def func(df_any):
+            ...     return df_any.group_by("a").agg(nw.col("b").sum()).sort("a")
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1502,8 +1496,8 @@ class DataFrame(BaseFrame[FrameT]):
             Group by multiple columns by passing a list of column names.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.group_by(["a", "b"]).agg(nw.max("c")).sort("a", "b")
+            ... def func(df_any):
+            ...     return df_any.group_by(["a", "b"]).agg(nw.max("c")).sort("a", "b")
             >>> func(df_pd)
                a  b  c
             0  a  1  5
@@ -1562,8 +1556,8 @@ class DataFrame(BaseFrame[FrameT]):
             columns in different orders
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.sort("c", "a", descending=[False, True])
+            ... def func(df_any):
+            ...     return df_any.sort("c", "a", descending=[False, True])
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1637,8 +1631,8 @@ class DataFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function in which we join over "ham" column:
 
             >>> @nw.narwhalify
-            ... def join_on_ham(df, other):
-            ...     return df.join(other, left_on="ham", right_on="ham")
+            ... def join_on_ham(df_any, other_any):
+            ...     return df_any.join(other_any, left_on="ham", right_on="ham")
 
             We can now pass either pandas or Polars to the function:
 
@@ -1684,10 +1678,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     duplicated = df.is_duplicated()
-            ...     return nw.to_native(duplicated)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.is_duplicated()
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1727,9 +1720,9 @@ class DataFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function that filters rows in which "foo"
             values are greater than 10, and then checks if the result is empty or not:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     return df.filter(nw.col("foo") > 10).is_empty()
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.filter(nw.col("foo") > 10).is_empty()
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1769,10 +1762,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     unique = df.is_unique()
-            ...     return nw.to_native(unique)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.is_unique()
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1830,10 +1822,9 @@ class DataFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function that returns the null count of
             each columns:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     null_counts = df.null_count()
-            ...     return nw.to_native(null_counts)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.null_count()
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1872,9 +1863,9 @@ class DataFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function that returns item at given row/column
 
-            >>> def func(df_any, row, column):
-            ...     df = nw.from_native(df_any)
-            ...     return df.item(row, column)
+            >>> @nw.narwhalify
+            ... def func(df_any, row, column):
+            ...     return df_any.item(row, column)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1901,8 +1892,8 @@ class DataFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function in which we clone the DataFrame:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.clone()
+            ... def func(df_any):
+            ...     return df_any.clone()
 
             >>> func(df_pd)
                a  b
@@ -2020,10 +2011,9 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.pipe(lambda _df: _df.select("a"))
-            ...     return nw.to_native(df)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.pipe(lambda _df: _df.select("a"))
 
             We can then pass either pandas or Polars:
 
@@ -2064,10 +2054,9 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.drop_nulls()
-            ...     return nw.to_native(df)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.drop_nulls()
 
             We can then pass either pandas or Polars:
 
@@ -2100,10 +2089,9 @@ class LazyFrame(BaseFrame[FrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.with_row_index()
-            ...     return nw.to_native(df)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.with_row_index()
 
             We can then pass either pandas or Polars:
 
@@ -2183,8 +2171,8 @@ class LazyFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.columns
+            ... def func(df_any):
+            ...     return df_any.columns
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2235,8 +2223,8 @@ class LazyFrame(BaseFrame[FrameT]):
             to add it as a new column:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns((nw.col("a") * 2).alias("2a"))
+            ... def func(df_any):
+            ...     return df_any.with_columns((nw.col("a") * 2).alias("2a"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2306,8 +2294,8 @@ class LazyFrame(BaseFrame[FrameT]):
             column to select that column.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select("foo")
+            ... def func(df_any):
+            ...     return df_any.select("foo")
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2342,8 +2330,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Multiple columns can be selected by passing a list of column names.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(["foo", "bar"])
+            ... def func(df_any):
+            ...     return df_any.select(["foo", "bar"])
             >>> func(df_pd)
                foo  bar
             0    1    6
@@ -2376,8 +2364,8 @@ class LazyFrame(BaseFrame[FrameT]):
             list. Expressions are also accepted.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("foo"), nw.col("bar") + 1)
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("foo"), nw.col("bar") + 1)
             >>> func(df_pd)
                foo  bar
             0    1    7
@@ -2409,8 +2397,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Use keyword arguments to easily name your expression inputs.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(threshold=nw.col("foo") * 2)
+            ... def func(df_any):
+            ...     return df_any.select(threshold=nw.col("foo") * 2)
             >>> func(df_pd)
                threshold
             0          2
@@ -2466,8 +2454,8 @@ class LazyFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.rename({"foo": "apple"})
+            ... def func(df_any):
+            ...     return df_any.rename({"foo": "apple"})
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2512,8 +2500,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function that gets the first 3 rows.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.head(3)
+            ... def func(df_any):
+            ...     return df_any.head(3)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2569,8 +2557,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function that gets the last 3 rows.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.tail(3)
+            ... def func(df_any):
+            ...     return df_any.tail(3)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2623,8 +2611,8 @@ class LazyFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.drop("ham")
+            ... def func(df_any):
+            ...     return df_any.drop("ham")
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2648,8 +2636,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Use positional arguments to drop multiple columns.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.drop("foo", "ham")
+            ... def func(df_any):
+            ...     return df_any.drop("foo", "ham")
 
             >>> func(df_pd)
                bar
@@ -2696,8 +2684,8 @@ class LazyFrame(BaseFrame[FrameT]):
             We define a library agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.unique(["bar", "ham"])
+            ... def func(df_any):
+            ...     return df_any.unique(["bar", "ham"])
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2729,21 +2717,21 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
-            >>> df = {
+            >>> data = {
             ...     "foo": [1, 2, 3],
             ...     "bar": [6, 7, 8],
             ...     "ham": ["a", "b", "c"],
             ... }
-            >>> df_pd = pd.DataFrame(df)
-            >>> df_pl = pl.DataFrame(df)
-            >>> lf_pl = pl.LazyFrame(df)
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.DataFrame(data)
+            >>> lf_pl = pl.LazyFrame(data)
 
             Let's define a dataframe-agnostic function in which we filter on
             one condition.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.filter(nw.col("foo") > 1)
+            ... def func(df_any):
+            ...     return df_any.filter(nw.col("foo") > 1)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2775,8 +2763,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Filter on multiple conditions:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.filter((nw.col("foo") < 3) & (nw.col("ham") == "a"))
+            ... def func(df_any):
+            ...     return df_any.filter((nw.col("foo") < 3) & (nw.col("ham") == "a"))
             >>> func(df_pd)
                foo  bar ham
             0    1    6   a
@@ -2802,8 +2790,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Provide multiple filters using `*args` syntax:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     dframe = df.filter(
+            ... def func(df_any):
+            ...     dframe = df_any.filter(
             ...         nw.col("foo") == 1,
             ...         nw.col("ham") == "a",
             ...     )
@@ -2833,8 +2821,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Filter on an OR condition:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.filter((nw.col("foo") == 1) | (nw.col("ham") == "c"))
+            ... def func(df_any):
+            ...     return df_any.filter((nw.col("foo") == 1) | (nw.col("ham") == "c"))
             >>> func(df_pd)
                foo  bar ham
             0    1    6   a
@@ -2891,8 +2879,8 @@ class LazyFrame(BaseFrame[FrameT]):
             and call `agg` to compute the grouped sum of another column.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.group_by("a").agg(nw.col("b").sum()).sort("a")
+            ... def func(df_any):
+            ...     return df_any.group_by("a").agg(nw.col("b").sum()).sort("a")
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2927,8 +2915,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Group by multiple columns by passing a list of column names.
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.group_by(["a", "b"]).agg(nw.max("c")).sort(["a", "b"])
+            ... def func(df_any):
+            ...     return df_any.group_by(["a", "b"]).agg(nw.max("c")).sort(["a", "b"])
             >>> func(df_pd)
                a  b  c
             0  a  1  5
@@ -3000,8 +2988,8 @@ class LazyFrame(BaseFrame[FrameT]):
             columns in different orders
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.sort("c", "a", descending=[False, True])
+            ... def func(df_any):
+            ...     return df_any.sort("c", "a", descending=[False, True])
 
             We can then pass either pandas or Polars to `func`:
 
@@ -3075,8 +3063,8 @@ class LazyFrame(BaseFrame[FrameT]):
             Let's define a dataframe-agnostic function in which we join over "ham" column:
 
             >>> @nw.narwhalify
-            ... def join_on_ham(df, other):
-            ...     return df.join(other, left_on="ham", right_on="ham")
+            ... def join_on_ham(df_any, other_any):
+            ...     return df_any.join(other_any, left_on="ham", right_on="ham")
 
             We can now pass either pandas or Polars to the function:
 
@@ -3112,8 +3100,8 @@ class LazyFrame(BaseFrame[FrameT]):
         Let's define a dataframe-agnostic function in which we copy the DataFrame:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.clone()
+        ... def func(df_any):
+        ...     return df_any.clone()
 
         >>> func(df_pd)
            a  b
diff --git a/narwhals/expression.py b/narwhals/expression.py
index 6aded757..d5297e10 100644
--- a/narwhals/expression.py
+++ b/narwhals/expression.py
@@ -266,8 +266,8 @@ class Expr:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").any())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").any())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -300,8 +300,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").all())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").all())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -334,8 +334,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").mean())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").mean())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -372,8 +372,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").std(ddof=0))
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").std(ddof=0))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -407,8 +407,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").sum())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").sum())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -439,9 +439,10 @@ class Expr:
             >>> df_pl = pl.DataFrame({"a": [1, 2], "b": [4, 3]})
 
             Let's define a dataframe-agnostic function:
+
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.min("a", "b"))
+            ... def func(df_any):
+            ...     return df_any.select(nw.min("a", "b"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -475,8 +476,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.max("a", "b"))
+            ... def func(df_any):
+            ...     return df_any.select(nw.max("a", "b"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -509,8 +510,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.all().count())
+            ... def func(df_any):
+            ...     return df_any.select(nw.all().count())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -543,8 +544,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").n_unique())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").n_unique())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -577,8 +578,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").unique())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").unique())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -616,8 +617,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").abs())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").abs())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -652,8 +653,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").cum_sum())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a", "b").cum_sum())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -703,8 +704,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(a_diff=nw.col("a").diff())
+            ... def func(df_any):
+            ...     return df_any.select(a_diff=nw.col("a").diff())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -754,8 +755,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(a_shift=nw.col("a").shift(n=1))
+            ... def func(df_any):
+            ...     return df_any.select(a_shift=nw.col("a").shift(n=1))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -800,8 +801,8 @@ class Expr:
             Let's define dataframe-agnostic functions:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").sort())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a").sort())
 
             >>> def func_descend(df_any):
             ...     df = nw.from_native(df_any)
@@ -874,8 +875,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").is_between(2, 4, "right"))
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a").is_between(2, 4, "right"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -921,8 +922,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(b=nw.col("a").is_in([1, 2]))
+            ... def func(df_any):
+            ...     return df_any.with_columns(b=nw.col("a").is_in([1, 2]))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -966,8 +967,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(
+            ... def func(df_any):
+            ...     return df_any.select(
             ...         nw.col("a").filter(nw.col("a") > 4),
             ...         nw.col("b").filter(nw.col("b") < 13),
             ...     )
@@ -1019,8 +1020,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         a_is_null=nw.col("a").is_null(), b_is_null=nw.col("b").is_null()
             ...     )
 
@@ -1072,8 +1073,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(nw.col("a", "b").fill_null(0))
+            ... def func(df_any):
+            ...     return df_any.with_columns(nw.col("a", "b").fill_null(0))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1121,8 +1122,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").drop_nulls())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a").drop_nulls())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1176,8 +1177,10 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").sample(fraction=1.0, with_replacement=True))
+            ... def func(df_any):
+            ...     return df_any.select(
+            ...         nw.col("a").sample(fraction=1.0, with_replacement=True)
+            ...     )
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1224,8 +1227,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(a_min_per_group=nw.col("a").min().over("b"))
+            ... def func(df_any):
+            ...     return df_any.with_columns(a_min_per_group=nw.col("a").min().over("b"))
 
             We can then pass either pandas or Polars:
 
@@ -1263,8 +1266,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.all().is_duplicated())
+            ... def func(df_any):
+            ...     return df_any.select(nw.all().is_duplicated())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1304,8 +1307,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.all().is_unique())
+            ... def func(df_any):
+            ...     return df_any.select(nw.all().is_unique())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1350,8 +1353,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.all().null_count())
+            ... def func(df_any):
+            ...     return df_any.select(nw.all().null_count())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1385,8 +1388,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.all().is_first_distinct())
+            ... def func(df_any):
+            ...     return df_any.select(nw.all().is_first_distinct())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1425,8 +1428,8 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.all().is_last_distinct())
+            ... def func(df_any):
+            ...     return df_any.select(nw.all().is_last_distinct())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1478,8 +1481,10 @@ class Expr:
             Let's define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a", "b").quantile(0.5, interpolation="linear"))
+            ... def func(df_any):
+            ...     return df_any.select(
+            ...         nw.col("a", "b").quantile(0.5, interpolation="linear")
+            ...     )
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1520,8 +1525,8 @@ class Expr:
             Let's define a dataframe-agnostic function that returns the first 3 rows:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").head(3))
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a").head(3))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1564,8 +1569,8 @@ class Expr:
             Let's define a dataframe-agnostic function that returns the last 3 rows:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").tail(3))
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a").tail(3))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1616,8 +1621,8 @@ class Expr:
             Let's define a dataframe-agnostic function that rounds to the first decimal:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").round(1))
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a").round(1))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1658,8 +1663,8 @@ class Expr:
             Let's define a dataframe-agnostic function that computes the len over different values of "b" column:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(
+            ... def func(df_any):
+            ...     return df_any.select(
             ...         nw.col("a").filter(nw.col("b") == 1).len().alias("a1"),
             ...         nw.col("a").filter(nw.col("b") == 2).len().alias("a2"),
             ...     )
@@ -1716,8 +1721,8 @@ class ExprCatNamespace:
             from column 'fruits':
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("fruits").cat.get_categories())
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("fruits").cat.get_categories())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1763,8 +1768,10 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(has_prefix=nw.col("fruits").str.starts_with("app"))
+            ... def func(df_any):
+            ...     return df_any.with_columns(
+            ...         has_prefix=nw.col("fruits").str.starts_with("app")
+            ...     )
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1808,8 +1815,10 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(has_suffix=nw.col("fruits").str.ends_with("ngo"))
+            ... def func(df_any):
+            ...     return df_any.with_columns(
+            ...         has_suffix=nw.col("fruits").str.ends_with("ngo")
+            ...     )
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1855,8 +1864,8 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         default_match=nw.col("pets").str.contains("parrot|Dove"),
             ...         case_insensitive_match=nw.col("pets").str.contains("(?i)parrot|Dove"),
             ...         literal_match=nw.col("pets").str.contains(
@@ -1912,8 +1921,8 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(s_sliced=nw.col("s").str.slice(4, length=3))
+            ... def func(df_any):
+            ...     return df_any.with_columns(s_sliced=nw.col("s").str.slice(4, length=3))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -1940,8 +1949,8 @@ class ExprStringNamespace:
             Using negative indexes:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(s_sliced=nw.col("s").str.slice(-3))
+            ... def func(df_any):
+            ...     return df_any.with_columns(s_sliced=nw.col("s").str.slice(-3))
 
             >>> func(df_pd)  # doctest: +NORMALIZE_WHITESPACE
                          s s_sliced
@@ -1988,8 +1997,8 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(lyrics_head=nw.col("lyrics").str.head())
+            ... def func(df_any):
+            ...     return df_any.with_columns(lyrics_head=nw.col("lyrics").str.head())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2036,8 +2045,8 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(lyrics_tail=nw.col("lyrics").str.tail())
+            ... def func(df_any):
+            ...     return df_any.with_columns(lyrics_tail=nw.col("lyrics").str.tail())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2088,8 +2097,8 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").str.to_datetime(format="%Y-%m-%d"))
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a").str.to_datetime(format="%Y-%m-%d"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2132,8 +2141,8 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(upper_col=nw.col("fruits").str.to_uppercase())
+            ... def func(df_any):
+            ...     return df_any.with_columns(upper_col=nw.col("fruits").str.to_uppercase())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2173,8 +2182,8 @@ class ExprStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(lower_col=nw.col("fruits").str.to_lowercase())
+            ... def func(df_any):
+            ...     return df_any.with_columns(lower_col=nw.col("fruits").str.to_lowercase())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2227,8 +2236,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(nw.col("datetime").dt.year().alias("year"))
+            ... def func(df_any):
+            ...     return df_any.with_columns(nw.col("datetime").dt.year().alias("year"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2275,8 +2284,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         nw.col("datetime").dt.year().alias("year"),
             ...         nw.col("datetime").dt.month().alias("month"),
             ...     )
@@ -2326,8 +2335,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         nw.col("datetime").dt.year().alias("year"),
             ...         nw.col("datetime").dt.month().alias("month"),
             ...         nw.col("datetime").dt.day().alias("day"),
@@ -2378,8 +2387,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(nw.col("datetime").dt.hour().alias("hour"))
+            ... def func(df_any):
+            ...     return df_any.with_columns(nw.col("datetime").dt.hour().alias("hour"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2426,8 +2435,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         nw.col("datetime").dt.hour().alias("hour"),
             ...         nw.col("datetime").dt.minute().alias("minute"),
             ...     )
@@ -2475,8 +2484,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         nw.col("datetime").dt.hour().alias("hour"),
             ...         nw.col("datetime").dt.minute().alias("minute"),
             ...         nw.col("datetime").dt.second().alias("second"),
@@ -2525,8 +2534,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         nw.col("datetime").dt.hour().alias("hour"),
             ...         nw.col("datetime").dt.minute().alias("minute"),
             ...         nw.col("datetime").dt.second().alias("second"),
@@ -2576,8 +2585,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         nw.col("datetime").dt.hour().alias("hour"),
             ...         nw.col("datetime").dt.minute().alias("minute"),
             ...         nw.col("datetime").dt.second().alias("second"),
@@ -2627,8 +2636,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         nw.col("datetime").dt.hour().alias("hour"),
             ...         nw.col("datetime").dt.minute().alias("minute"),
             ...         nw.col("datetime").dt.second().alias("second"),
@@ -2672,8 +2681,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(a_ordinal_day=nw.col("a").dt.ordinal_day())
+            ... def func(df_any):
+            ...     return df_any.with_columns(a_ordinal_day=nw.col("a").dt.ordinal_day())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2715,8 +2724,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(a_total_minutes=nw.col("a").dt.total_minutes())
+            ... def func(df_any):
+            ...     return df_any.with_columns(a_total_minutes=nw.col("a").dt.total_minutes())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2758,8 +2767,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(a_total_seconds=nw.col("a").dt.total_seconds())
+            ... def func(df_any):
+            ...     return df_any.with_columns(a_total_seconds=nw.col("a").dt.total_seconds())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2806,8 +2815,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         a_total_milliseconds=nw.col("a").dt.total_milliseconds()
             ...     )
 
@@ -2858,8 +2867,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         a_total_microseconds=nw.col("a").dt.total_microseconds()
             ...     )
 
@@ -2907,8 +2916,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(
+            ... def func(df_any):
+            ...     return df_any.with_columns(
             ...         a_diff_total_nanoseconds=nw.col("a").diff().dt.total_nanoseconds()
             ...     )
 
@@ -2984,8 +2993,8 @@ class ExprDateTimeNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.select(nw.col("a").dt.to_string("%Y/%m/%d %H:%M:%S"))
+            ... def func(df_any):
+            ...     return df_any.select(nw.col("a").dt.to_string("%Y/%m/%d %H:%M:%S"))
 
             We can then pass either pandas or Polars to `func`:
 
@@ -3029,8 +3038,8 @@ def col(*names: str | Iterable[str]) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.col("a") * nw.col("b"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.col("a") * nw.col("b"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -3070,8 +3079,8 @@ def all() -> Expr:
         Let's define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.all() * 2)
+        ... def func(df_any):
+        ...     return df_any.select(nw.all() * 2)
 
         We can then pass either pandas or Polars to `func`:
 
@@ -3109,8 +3118,8 @@ def len() -> Expr:
         Let's define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.len())
+        ... def func(df_any):
+        ...     return df_any.select(nw.len())
 
         We can then pass either pandas or Polars to `func`:
 
@@ -3154,8 +3163,8 @@ def sum(*columns: str) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.sum("a"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.sum("a"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -3195,10 +3204,9 @@ def mean(*columns: str) -> Expr:
 
         We define a dataframe agnostic function:
 
-        >>> def func(df_any):
-        ...     df = nw.from_native(df_any)
-        ...     df = df.select(nw.mean("a"))
-        ...     return nw.to_native(df)
+        >>> @nw.narwhalify
+        ... def func(df_any):
+        ...     return df_any.select(nw.mean("a"))
 
         We can then pass either pandas or Polars to `func`:
 
@@ -3239,8 +3247,8 @@ def min(*columns: str) -> Expr:
         Let's define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.min("b"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.min("b"))
 
         We can then pass either pandas or Polars to `func`:
 
@@ -3280,8 +3288,8 @@ def max(*columns: str) -> Expr:
         Let's define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.max("a"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.max("a"))
 
         We can then pass either pandas or Polars to `func`:
 
@@ -3318,8 +3326,8 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.sum_horizontal("a", "b"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.sum_horizontal("a", "b"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -3371,8 +3379,8 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select("a", "b", all=nw.all_horizontal("a", "b"))
+        ... def func(df_any):
+        ...     return df_any.select("a", "b", all=nw.all_horizontal("a", "b"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -3425,8 +3433,8 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.with_columns(nw.lit(3).alias("b"))
+        ... def func(df_any):
+        ...     return df_any.with_columns(nw.lit(3).alias("b"))
 
         We can then pass either pandas or polars to `func`:
 
diff --git a/narwhals/group_by.py b/narwhals/group_by.py
index ed5f6281..69995cba 100644
--- a/narwhals/group_by.py
+++ b/narwhals/group_by.py
@@ -62,15 +62,13 @@ class GroupBy(Generic[DataFrameT]):
 
             We define library agnostic functions:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.group_by("a").agg(nw.col("b").sum()).sort("a")
-            ...     return nw.to_native(df)
-
-            >>> def func_mult_col(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.group_by("a", "b").agg(nw.sum("c")).sort("a", "b")
-            ...     return nw.to_native(df)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.group_by("a").agg(nw.col("b").sum()).sort("a")
+
+            >>> @nw.narwhalify
+            ... def func_mult_col(df_any):
+            ...     return df_any.group_by("a", "b").agg(nw.sum("c")).sort("a", "b")
 
             We can then pass either pandas or Polars to `func` and `func_mult_col`:
 
diff --git a/narwhals/selectors.py b/narwhals/selectors.py
index c6f45b94..6c512591 100644
--- a/narwhals/selectors.py
+++ b/narwhals/selectors.py
@@ -30,10 +30,9 @@ def by_dtype(*dtypes: Any) -> Expr:
         Let's define a dataframe-agnostic function to select int64 and float64
         dtypes and multiplies each value by 2:
 
-        >>> def func(df_any):
-        ...     df = nw.from_native(df_any)
-        ...     df = df.select(ncs.by_dtype(nw.Int64, nw.Float64) * 2)
-        ...     return nw.to_native(df)
+        >>> @nw.narwhalify
+        ... def func(df_any):
+        ...     return df_any.select(ncs.by_dtype(nw.Int64, nw.Float64) * 2)
 
         We can then pass either pandas or Polars dataframes:
 
@@ -76,10 +75,9 @@ def numeric() -> Expr:
         Let's define a dataframe-agnostic function to select numeric
         dtypes and multiplies each value by 2:
 
-        >>> def func(df_any):
-        ...     df = nw.from_native(df_any)
-        ...     df = df.select(ncs.numeric() * 2)
-        ...     return nw.to_native(df)
+        >>> @nw.narwhalify
+        ... def func(df_any):
+        ...     return df_any.select(ncs.numeric() * 2)
 
         We can then pass either pandas or Polars dataframes:
 
@@ -118,10 +116,9 @@ def boolean() -> Expr:
         Let's define a dataframe-agnostic function to select boolean
         dtypes:
 
-        >>> def func(df_any):
-        ...     df = nw.from_native(df_any)
-        ...     df = df.select(ncs.boolean())
-        ...     return nw.to_native(df)
+        >>> @nw.narwhalify
+        ... def func(df_any):
+        ...     return df_any.select(ncs.boolean())
 
         We can then pass either pandas or Polars dataframes:
 
@@ -160,10 +157,9 @@ def string() -> Expr:
         Let's define a dataframe-agnostic function to select string
         dtypes:
 
-        >>> def func(df_any):
-        ...     df = nw.from_native(df_any)
-        ...     df = df.select(ncs.string())
-        ...     return nw.to_native(df)
+        >>> @nw.narwhalify
+        ... def func(df_any):
+        ...     return df_any.select(ncs.string())
 
         We can then pass either pandas or Polars dataframes:
 
@@ -202,10 +198,9 @@ def categorical() -> Expr:
         Let's define a dataframe-agnostic function to select string
         dtypes:
 
-        >>> def func(df_any):
-        ...     df = nw.from_native(df_any)
-        ...     df = df.select(ncs.categorical())
-        ...     return nw.to_native(df)
+        >>> @nw.narwhalify
+        ... def func(df_any):
+        ...     return df_any.select(ncs.categorical())
 
         We can then pass either pandas or Polars dataframes:
 
@@ -244,10 +239,9 @@ def all() -> Expr:
         Let's define a dataframe-agnostic function to select string
         dtypes:
 
-        >>> def func(df_any):
-        ...     df = nw.from_native(df_any)
-        ...     df = df.select(ncs.all())
-        ...     return nw.to_native(df)
+        >>> @nw.narwhalify
+        ... def func(df_any):
+        ...     return df_any.select(ncs.all())
 
         We can then pass either pandas or Polars dataframes:
 
diff --git a/narwhals/series.py b/narwhals/series.py
index 027c4e83..aa1c2524 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -2000,8 +2000,8 @@ class SeriesStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(series):
-            ...     return series.str.starts_with("app")
+            ... def func(s_any):
+            ...     return s_any.str.starts_with("app")
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2042,8 +2042,8 @@ class SeriesStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(series):
-            ...     return series.str.ends_with("ngo")
+            ... def func(s_any):
+            ...     return s_any.str.ends_with("ngo")
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2301,8 +2301,8 @@ class SeriesStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(upper_col=nw.col("fruits").str.to_uppercase())
+            ... def func(df_any):
+            ...     return df_any.with_columns(upper_col=nw.col("fruits").str.to_uppercase())
 
             We can then pass either pandas or Polars to `func`:
 
@@ -2344,8 +2344,8 @@ class SeriesStringNamespace:
             We define a dataframe-agnostic function:
 
             >>> @nw.narwhalify
-            ... def func(df):
-            ...     return df.with_columns(lower_col=nw.col("fruits").str.to_lowercase())
+            ... def func(df_any):
+            ...     return df_any.with_columns(lower_col=nw.col("fruits").str.to_lowercase())
 
             We can then pass either pandas or Polars to `func`:
 
diff --git a/narwhals/stable/v1.py b/narwhals/stable/v1.py
index 959e7d95..89f8ba44 100644
--- a/narwhals/stable/v1.py
+++ b/narwhals/stable/v1.py
@@ -149,10 +149,9 @@ class DataFrame(NwDataFrame[IntoDataFrameT]):
 
             We define a library agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     df = df.to_dict(as_series=False)
-            ...     return df
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.to_dict(as_series=False)
 
             We can then pass either pandas or Polars to `func`:
 
@@ -188,10 +187,9 @@ class DataFrame(NwDataFrame[IntoDataFrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     duplicated = df.is_duplicated()
-            ...     return nw.to_native(duplicated)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.is_duplicated()
 
             We can then pass either pandas or Polars to `func`:
 
@@ -237,10 +235,9 @@ class DataFrame(NwDataFrame[IntoDataFrameT]):
 
             Let's define a dataframe-agnostic function:
 
-            >>> def func(df_any):
-            ...     df = nw.from_native(df_any)
-            ...     unique = df.is_unique()
-            ...     return nw.to_native(unique)
+            >>> @nw.narwhalify
+            ... def func(df_any):
+            ...     return df_any.is_unique()
 
             We can then pass either pandas or Polars to `func`:
 
@@ -750,8 +747,8 @@ def narwhalify(
 
 
     @nw.narwhalify
-    def func(df):
-        return df.group_by("a").agg(nw.col("b").sum())
+    def func(df_any):
+        return df_any.group_by("a").agg(nw.col("b").sum())
     ```
 
     You can also pass in extra arguments, e.g.
@@ -836,8 +833,8 @@ def all() -> Expr:
         Let's define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.all() * 2)
+        ... def func(df_any):
+        ...     return df_any.select(nw.all() * 2)
 
         We can then pass either pandas or Polars to `func`:
 
@@ -878,8 +875,8 @@ def col(*names: str | Iterable[str]) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.col("a") * nw.col("b"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.col("a") * nw.col("b"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -915,8 +912,8 @@ def len() -> Expr:
         Let's define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.len())
+        ... def func(df_any):
+        ...     return df_any.select(nw.len())
 
         We can then pass either pandas or Polars to `func`:
 
@@ -954,8 +951,8 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.with_columns(nw.lit(3).alias("b"))
+        ... def func(df_any):
+        ...     return df_any.with_columns(nw.lit(3).alias("b"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -998,8 +995,8 @@ def min(*columns: str) -> Expr:
         Let's define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.min("b"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.min("b"))
 
         We can then pass either pandas or Polars to `func`:
 
@@ -1039,8 +1036,8 @@ def max(*columns: str) -> Expr:
         Let's define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.max("a"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.max("a"))
 
         We can then pass either pandas or Polars to `func`:
 
@@ -1079,10 +1076,9 @@ def mean(*columns: str) -> Expr:
 
         We define a dataframe agnostic function:
 
-        >>> def func(df_any):
-        ...     df = nw.from_native(df_any)
-        ...     df = df.select(nw.mean("a"))
-        ...     return nw.to_native(df)
+        >>> @nw.narwhalify
+        ... def func(df_any):
+        ...     return df_any.select(nw.mean("a"))
 
         We can then pass either pandas or Polars to `func`:
 
@@ -1122,8 +1118,8 @@ def sum(*columns: str) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.sum("a"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.sum("a"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -1160,8 +1156,8 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select(nw.sum_horizontal("a", "b"))
+        ... def func(df_any):
+        ...     return df_any.select(nw.sum_horizontal("a", "b"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -1209,8 +1205,8 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         We define a dataframe-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.select("a", "b", all=nw.all_horizontal("a", "b"))
+        ... def func(df_any):
+        ...     return df_any.select("a", "b", all=nw.all_horizontal("a", "b"))
 
         We can then pass either pandas or polars to `func`:
 
@@ -1268,8 +1264,8 @@ def is_ordered_categorical(series: Series) -> bool:
         Let's define a library-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(s):
-        ...     return nw.is_ordered_categorical(s)
+        ... def func(s_any):
+        ...     return nw.is_ordered_categorical(s_any)
 
         Then, we can pass any supported library to `func`:
 
diff --git a/narwhals/translate.py b/narwhals/translate.py
index c0389332..24481d20 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -535,8 +535,8 @@ def narwhalify(
 
 
     @nw.narwhalify
-    def func(df):
-        return df.group_by("a").agg(nw.col("b").sum())
+    def func(df_any):
+        return df_any.group_by("a").agg(nw.col("b").sum())
     ```
 
     You can also pass in extra arguments, e.g.
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 0edfe235..c586dfcf 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import re
+import secrets
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
@@ -285,8 +286,8 @@ def is_ordered_categorical(series: Series) -> bool:
         Let's define a library-agnostic function:
 
         >>> @nw.narwhalify
-        ... def func(s):
-        ...     return nw.is_ordered_categorical(s)
+        ... def func(s_any):
+        ...     return nw.is_ordered_categorical(s_any)
 
         Then, we can pass any supported library to `func`:
 
@@ -325,3 +326,31 @@ def is_ordered_categorical(series: Series) -> bool:
         return native_series.type.ordered  # type: ignore[no-any-return]
     # If it doesn't match any of the above, let's just play it safe and return False.
     return False  # pragma: no cover
+
+
+def generate_unique_token(n_bytes: int, columns: list[str]) -> str:  # pragma: no cover
+    """Generates a unique token of specified n_bytes that is not present in the given list of columns.
+
+    Arguments:
+        n_bytes : The number of bytes to generate for the token.
+        columns : The list of columns to check for uniqueness.
+
+    Returns:
+        A unique token that is not present in the given list of columns.
+
+    Raises:
+        AssertionError: If a unique token cannot be generated after 100 attempts.
+    """
+    counter = 0
+    while True:
+        token = secrets.token_hex(n_bytes)
+        if token not in columns:
+            return token
+
+        counter += 1
+        if counter > 100:
+            msg = (
+                "Internal Error: Narwhals was not able to generate a column name to perform given "
+                "join operation"
+            )
+            raise AssertionError(msg)
diff --git a/utils/check_backend_completeness.py b/utils/check_backend_completeness.py
index 41466a15..1c74f68a 100644
--- a/utils/check_backend_completeness.py
+++ b/utils/check_backend_completeness.py
@@ -18,7 +18,6 @@ MISSING = [
     "DataFrame.pipe",
     "DataFrame.unique",
     "Series.drop_nulls",
-    "Series.fill_null",
     "Series.from_iterable",
     "Series.is_between",
     "Series.is_duplicated",


Test patch:
diff --git a/tests/expr_and_series/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py
index 31ad6518..e43d0f30 100644
--- a/tests/expr_and_series/arithmetic_test.py
+++ b/tests/expr_and_series/arithmetic_test.py
@@ -29,7 +29,6 @@ def test_arithmetic(
     # pyarrow case
     if "pyarrow_table" in str(constructor) and attr in {
         "__truediv__",
-        "__floordiv__",
         "__mod__",
     }:
         request.applymarker(pytest.mark.xfail)
@@ -61,7 +60,6 @@ def test_right_arithmetic(
     # pyarrow case
     if "table" in str(constructor) and attr in {
         "__rtruediv__",
-        "__rfloordiv__",
         "__rmod__",
     }:
         request.applymarker(pytest.mark.xfail)
diff --git a/tests/expr_and_series/fill_null_test.py b/tests/expr_and_series/fill_null_test.py
index 5cffcddd..dc9cc738 100644
--- a/tests/expr_and_series/fill_null_test.py
+++ b/tests/expr_and_series/fill_null_test.py
@@ -1,7 +1,5 @@
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
@@ -12,10 +10,7 @@ data = {
 }
 
 
-def test_fill_null(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_fill_null(constructor: Any) -> None:
     df = nw.from_native(constructor(data), eager_only=True)
 
     result = df.with_columns(nw.col("a", "b", "c").fill_null(99))
diff --git a/tests/expr_and_series/str/contains_test.py b/tests/expr_and_series/str/contains_test.py
index 32e45de0..831aa82d 100644
--- a/tests/expr_and_series/str/contains_test.py
+++ b/tests/expr_and_series/str/contains_test.py
@@ -2,7 +2,6 @@ from typing import Any
 
 import pandas as pd
 import polars as pl
-import pytest
 
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
@@ -13,9 +12,8 @@ df_pandas = pd.DataFrame(data)
 df_polars = pl.DataFrame(data)
 
 
-@pytest.mark.parametrize("df_any", [df_pandas, df_polars])
-def test_contains(df_any: Any) -> None:
-    df = nw.from_native(df_any, eager_only=True)
+def test_contains(constructor: Any) -> None:
+    df = nw.from_native(constructor(data), eager_only=True)
     result = df.with_columns(
         case_insensitive_match=nw.col("pets").str.contains("(?i)parrot|Dove")
     )
diff --git a/tests/expr_and_series/str/head_test.py b/tests/expr_and_series/str/head_test.py
index 9fe2638f..9deb2862 100644
--- a/tests/expr_and_series/str/head_test.py
+++ b/tests/expr_and_series/str/head_test.py
@@ -1,16 +1,12 @@
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 data = {"a": ["foo", "bars"]}
 
 
-def test_str_head(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_str_head(constructor: Any) -> None:
     df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(nw.col("a").str.head(3))
     expected = {
diff --git a/tests/expr_and_series/str/slice_test.py b/tests/expr_and_series/str/slice_test.py
index 946f4e35..e3831952 100644
--- a/tests/expr_and_series/str/slice_test.py
+++ b/tests/expr_and_series/str/slice_test.py
@@ -15,11 +15,8 @@ data = {"a": ["fdas", "edfas"]}
     [(1, 2, {"a": ["da", "df"]}), (-2, None, {"a": ["as", "as"]})],
 )
 def test_str_slice(
-    request: Any, constructor: Any, offset: int, length: int | None, expected: Any
+    constructor: Any, offset: int, length: int | None, expected: Any
 ) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     df = nw.from_native(constructor(data), eager_only=True)
     result_frame = df.select(nw.col("a").str.slice(offset, length))
     compare_dicts(result_frame, expected)
diff --git a/tests/expr_and_series/str/starts_with_ends_with_test.py b/tests/expr_and_series/str/starts_with_ends_with_test.py
index ee477461..9216f2e6 100644
--- a/tests/expr_and_series/str/starts_with_ends_with_test.py
+++ b/tests/expr_and_series/str/starts_with_ends_with_test.py
@@ -2,8 +2,6 @@ from __future__ import annotations
 
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 
 # Don't move this into typechecking block, for coverage
@@ -13,10 +11,7 @@ from tests.utils import compare_dicts
 data = {"a": ["fdas", "edfas"]}
 
 
-def test_ends_with(request: Any, constructor_with_lazy: Any) -> None:
-    if "pyarrow_table" in str(constructor_with_lazy):
-        request.applymarker(pytest.mark.xfail)
-
+def test_ends_with(constructor_with_lazy: Any) -> None:
     df = nw.from_native(constructor_with_lazy(data)).lazy()
     result = df.select(nw.col("a").str.ends_with("das"))
     expected = {
@@ -31,10 +26,7 @@ def test_ends_with(request: Any, constructor_with_lazy: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_starts_with(request: Any, constructor_with_lazy: Any) -> None:
-    if "pyarrow_table" in str(constructor_with_lazy):
-        request.applymarker(pytest.mark.xfail)
-
+def test_starts_with(constructor_with_lazy: Any) -> None:
     df = nw.from_native(constructor_with_lazy(data)).lazy()
     result = df.select(nw.col("a").str.starts_with("fda"))
     expected = {
diff --git a/tests/expr_and_series/str/tail_test.py b/tests/expr_and_series/str/tail_test.py
index b7c187e1..f116e445 100644
--- a/tests/expr_and_series/str/tail_test.py
+++ b/tests/expr_and_series/str/tail_test.py
@@ -1,17 +1,12 @@
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 data = {"a": ["foo", "bars"]}
 
 
-def test_str_tail(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_str_tail(constructor: Any) -> None:
     df = nw.from_native(constructor(data), eager_only=True)
     expected = {"a": ["foo", "ars"]}
 
diff --git a/tests/expr_and_series/str/to_datetime_test.py b/tests/expr_and_series/str/to_datetime_test.py
index 62c438f8..ea248bde 100644
--- a/tests/expr_and_series/str/to_datetime_test.py
+++ b/tests/expr_and_series/str/to_datetime_test.py
@@ -1,19 +1,15 @@
 from datetime import datetime
 from typing import Any
 
-import pandas as pd
-import polars as pl
-import pytest
-
 import narwhals.stable.v1 as nw
 
-df_pandas = pd.DataFrame({"a": ["2020-01-01T12:34:56"]})
-df_polars = pl.DataFrame({"a": ["2020-01-01T12:34:56"]})
+data = {"a": ["2020-01-01T12:34:56"]}
 
 
-@pytest.mark.parametrize("df_any", [df_pandas, df_polars])
-def test_to_datetime(df_any: Any) -> None:
-    result = nw.from_native(df_any, eager_only=True).select(
-        b=nw.col("a").str.to_datetime(format="%Y-%m-%dT%H:%M:%S")
-    )["b"][0]
+def test_to_datetime(constructor: Any) -> None:
+    result = (
+        nw.from_native(constructor(data), eager_only=True)
+        .select(b=nw.col("a").str.to_datetime(format="%Y-%m-%dT%H:%M:%S"))
+        .item(row=0, column="b")
+    )
     assert result == datetime(2020, 1, 1, 12, 34, 56)
diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py
index 6264f8ea..d8ab380c 100644
--- a/tests/frame/join_test.py
+++ b/tests/frame/join_test.py
@@ -37,15 +37,11 @@ def test_inner_join_single_key(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_cross_join(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_cross_join(constructor: Any) -> None:
     data = {"a": [1, 3, 2]}
     df = nw.from_native(constructor(data))
-    result = df.join(df, how="cross")  # type: ignore[arg-type]
-
-    expected = {"a": [1, 1, 1, 3, 3, 3, 2, 2, 2], "a_right": [1, 3, 2, 1, 3, 2, 1, 3, 2]}
+    result = df.join(df, how="cross").sort("a", "a_right")  # type: ignore[arg-type]
+    expected = {"a": [1, 1, 1, 2, 2, 2, 3, 3, 3], "a_right": [1, 2, 3, 1, 2, 3, 1, 2, 3]}
     compare_dicts(result, expected)
 
     with pytest.raises(ValueError, match="Can not pass left_on, right_on for cross join"):
@@ -71,15 +67,11 @@ def test_cross_join_non_pandas() -> None:
     ],
 )
 def test_anti_join(
-    request: Any,
     constructor: Any,
     join_key: list[str],
     filter_expr: nw.Expr,
     expected: dict[str, list[Any]],
 ) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     other = df.filter(filter_expr)
@@ -96,15 +88,11 @@ def test_anti_join(
     ],
 )
 def test_semi_join(
-    request: Any,
     constructor: Any,
     join_key: list[str],
     filter_expr: nw.Expr,
     expected: dict[str, list[Any]],
 ) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     other = df.filter(filter_expr)
@@ -127,10 +115,7 @@ def test_join_not_implemented(constructor: Any, how: str) -> None:
 
 
 @pytest.mark.filterwarnings("ignore:the default coalesce behavior")
-def test_left_join(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_left_join(constructor: Any) -> None:
     data_left = {"a": [1.0, 2, 3], "b": [4.0, 5, 6]}
     data_right = {"a": [1.0, 2, 3], "c": [4.0, 5, 7]}
     df_left = nw.from_native(constructor(data_left), eager_only=True)
@@ -143,10 +128,7 @@ def test_left_join(request: Any, constructor: Any) -> None:
 
 
 @pytest.mark.filterwarnings("ignore: the default coalesce behavior")
-def test_left_join_multiple_column(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_left_join_multiple_column(constructor: Any) -> None:
     data_left = {"a": [1, 2, 3], "b": [4, 5, 6]}
     data_right = {"a": [1, 2, 3], "c": [4, 5, 6]}
     df_left = nw.from_native(constructor(data_left), eager_only=True)
@@ -157,12 +139,9 @@ def test_left_join_multiple_column(request: Any, constructor: Any) -> None:
 
 
 @pytest.mark.filterwarnings("ignore: the default coalesce behavior")
-def test_left_join_overlapping_column(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
-    data_left = {"a": [1, 2, 3], "b": [4, 5, 6], "d": [1, 4, 2]}
-    data_right = {"a": [1, 2, 3], "c": [4, 5, 6], "d": [1, 4, 2]}
+def test_left_join_overlapping_column(constructor: Any) -> None:
+    data_left = {"a": [1.0, 2, 3], "b": [4.0, 5, 6], "d": [1.0, 4, 2]}
+    data_right = {"a": [1.0, 2, 3], "c": [4.0, 5, 6], "d": [1.0, 4, 2]}
     df_left = nw.from_native(constructor(data_left), eager_only=True)
     df_right = nw.from_native(constructor(data_right), eager_only=True)
     result = df_left.join(df_right, left_on="b", right_on="c", how="left")
diff --git a/tests/hypothesis/test_join.py b/tests/hypothesis/test_join.py
index d5db53e3..850caa1a 100644
--- a/tests/hypothesis/test_join.py
+++ b/tests/hypothesis/test_join.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import pandas as pd
 import polars as pl
+import pyarrow as pa
 import pytest
 from hypothesis import assume
 from hypothesis import given
@@ -163,3 +164,18 @@ def test_left_join(  # pragma: no cover
         )
     ).select(pl.all().fill_null(float("nan")))
     compare_dicts(result_pd.to_dict(as_series=False), result_pl.to_dict(as_series=False))
+    # For PyArrow, insert an extra sort, as the order of rows isn't guaranteed
+    result_pa = (
+        nw.from_native(pa.table(data_left), eager_only=True)
+        .join(
+            nw.from_native(pa.table(data_right), eager_only=True),
+            how="left",
+            left_on=left_key,
+            right_on=right_key,
+        )
+        .select(nw.all().cast(nw.Float64).fill_null(float("nan")))
+        .pipe(lambda df: df.sort(df.columns))
+    )
+    compare_dicts(
+        result_pa, result_pd.pipe(lambda df: df.sort(df.columns)).to_dict(as_series=False)
+    )
diff --git a/tests/series_only/arithmetic_test.py b/tests/series_only/arithmetic_test.py
index 9eca8c08..16a7e412 100644
--- a/tests/series_only/arithmetic_test.py
+++ b/tests/series_only/arithmetic_test.py
@@ -2,9 +2,17 @@ from __future__ import annotations
 
 from typing import Any
 
+import hypothesis.strategies as st
+import pandas as pd
+import polars as pl
+import pyarrow as pa
 import pytest
+from hypothesis import assume
+from hypothesis import given
 
 import narwhals.stable.v1 as nw
+from narwhals.utils import parse_version
+from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("data", [[1, 2, 3], [1.0, 2, 3]])
@@ -34,7 +42,6 @@ def test_arithmetic(
 
     if "pyarrow_series" in str(constructor_series) and attr in {
         "__truediv__",
-        "__floordiv__",
         "__mod__",
     }:
         request.applymarker(pytest.mark.xfail)
@@ -42,3 +49,40 @@ def test_arithmetic(
     s = nw.from_native(constructor_series(data), series_only=True)
     result = getattr(s, attr)(rhs)
     assert result.to_numpy().tolist() == expected
+
+
+@pytest.mark.slow()
+@given(  # type: ignore[misc]
+    left=st.integers(-100, 100),
+    right=st.integers(-100, 100),
+)
+def test_mod(left: int, right: int) -> None:
+    # hypothesis complains if we add `constructor` as an argument, so this
+    # test is a bit manual unfortunately
+    assume(right != 0)
+    expected = {"a": [left // right]}
+    result = nw.from_native(pd.DataFrame({"a": [left]}), eager_only=True).select(
+        nw.col("a") // right
+    )
+    compare_dicts(result, expected)
+    if parse_version(pd.__version__) < (2, 2):  # pragma: no cover
+        # Bug in old version of pandas
+        pass
+    else:
+        result = nw.from_native(
+            pd.DataFrame({"a": [left]}).convert_dtypes(dtype_backend="pyarrow"),
+            eager_only=True,
+        ).select(nw.col("a") // right)
+        compare_dicts(result, expected)
+    result = nw.from_native(
+        pd.DataFrame({"a": [left]}).convert_dtypes(), eager_only=True
+    ).select(nw.col("a") // right)
+    compare_dicts(result, expected)
+    result = nw.from_native(pl.DataFrame({"a": [left]}), eager_only=True).select(
+        nw.col("a") // right
+    )
+    compare_dicts(result, expected)
+    result = nw.from_native(pa.table({"a": [left]}), eager_only=True).select(
+        nw.col("a") // right
+    )
+    compare_dicts(result, expected)
diff --git a/tests/test_selectors.py b/tests/test_selectors.py
index 3ab5c5e0..41c721cb 100644
--- a/tests/test_selectors.py
+++ b/tests/test_selectors.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 from typing import Any
 
 import pandas as pd
-import polars as pl
+import pyarrow as pa
 import pytest
 
 import narwhals.stable.v1 as nw
@@ -13,6 +13,7 @@ from narwhals.selectors import by_dtype
 from narwhals.selectors import categorical
 from narwhals.selectors import numeric
 from narwhals.selectors import string
+from narwhals.utils import parse_version
 from tests.utils import compare_dicts
 
 data = {
@@ -23,54 +24,43 @@ data = {
 }
 
 
-def test_selectors(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_selectors(constructor: Any) -> None:
     df = nw.from_native(constructor(data))
-    result = nw.to_native(df.select(by_dtype([nw.Int64, nw.Float64]) + 1))
+    result = df.select(by_dtype([nw.Int64, nw.Float64]) + 1)
     expected = {"a": [2, 2, 3], "c": [5.1, 6.0, 7.0]}
     compare_dicts(result, expected)
 
 
-def test_numeric(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_numeric(constructor: Any) -> None:
     df = nw.from_native(constructor(data))
-    result = nw.to_native(df.select(numeric() + 1))
+    result = df.select(numeric() + 1)
     expected = {"a": [2, 2, 3], "c": [5.1, 6.0, 7.0]}
     compare_dicts(result, expected)
 
 
-def test_boolean(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_boolean(constructor: Any) -> None:
     df = nw.from_native(constructor(data))
-    result = nw.to_native(df.select(boolean()))
+    result = df.select(boolean())
     expected = {"d": [True, False, True]}
     compare_dicts(result, expected)
 
 
-def test_string(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_string(constructor: Any) -> None:
     df = nw.from_native(constructor(data))
-    result = nw.to_native(df.select(string()))
+    result = df.select(string())
     expected = {"b": ["a", "b", "c"]}
     compare_dicts(result, expected)
 
 
-def test_categorical() -> None:
-    df = nw.from_native(pd.DataFrame(data).astype({"b": "category"}))
-    result = nw.to_native(df.select(categorical()))
-    expected = {"b": ["a", "b", "c"]}
-    compare_dicts(result, expected)
-    df = nw.from_native(pl.DataFrame(data, schema_overrides={"b": pl.Categorical}))
-    result = nw.to_native(df.select(categorical()))
+def test_categorical(request: Any, constructor: Any) -> None:
+    if "pyarrow_table_constructor" in str(constructor) and parse_version(
+        pa.__version__
+    ) <= (15,):  # pragma: no cover
+        request.applymarker(pytest.mark.xfail)
     expected = {"b": ["a", "b", "c"]}
+
+    df = nw.from_native(constructor(data)).with_columns(nw.col("b").cast(nw.Categorical))
+    result = df.select(categorical())
     compare_dicts(result, expected)
 
 
@@ -89,18 +79,16 @@ def test_categorical() -> None:
     ],
 )
 def test_set_ops(
-    request: Any, constructor: Any, selector: nw.selectors.Selector, expected: list[str]
+    constructor: Any, selector: nw.selectors.Selector, expected: list[str]
 ) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     df = nw.from_native(constructor(data))
     result = df.select(selector).columns
     assert sorted(result) == expected
 
 
-def test_set_ops_invalid() -> None:
-    df = nw.from_native(pd.DataFrame(data))
+@pytest.mark.parametrize("invalid_constructor", [pd.DataFrame, pa.table])
+def test_set_ops_invalid(invalid_constructor: Any) -> None:
+    df = nw.from_native(invalid_constructor(data))
     with pytest.raises(NotImplementedError):
         df.select(1 - numeric())
     with pytest.raises(NotImplementedError):