Repository: narwhals-dev/narwhals
Pull request #936 (2024-09-09 19:51:20)

[pre-commit.ci] pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.3 → v0.6.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.3...v0.6.4)
<!--pre-commit.ci end-->


Patch:
diff --git a/.github/workflows/check_docs_build.yml b/.github/workflows/check_docs_build.yml
index 3b418baf..c59e67f4 100644
--- a/.github/workflows/check_docs_build.yml
+++ b/.github/workflows/check_docs_build.yml
@@ -18,8 +18,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install-reqs
         run: uv pip install --upgrade tox virtualenv setuptools pip -r requirements-dev.txt --system
       - name: install-docs-reqs
diff --git a/.github/workflows/check_tpch_queries.yml b/.github/workflows/check_tpch_queries.yml
index 46dd5df2..509ebb95 100644
--- a/.github/workflows/check_tpch_queries.yml
+++ b/.github/workflows/check_tpch_queries.yml
@@ -18,8 +18,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv 
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install-reqs
         run: uv pip install --upgrade -r requirements-dev.txt --system
       - name: local-install
@@ -27,4 +31,4 @@ jobs:
       - name: generate-data
         run: cd tpch && python generate_data.py
       - name: tpch-tests 
-        run: cd tpch && pytest tests
\ No newline at end of file
+        run: cd tpch && pytest tests
diff --git a/.github/workflows/extremes.yml b/.github/workflows/extremes.yml
index 7e1a5586..cec9d32d 100644
--- a/.github/workflows/extremes.yml
+++ b/.github/workflows/extremes.yml
@@ -18,8 +18,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install-minimum-versions
         run: uv pip install tox virtualenv setuptools pandas==0.25.3 polars==0.20.3 numpy==1.17.5 pyarrow==11.0.0 scipy==1.5.0 scikit-learn==1.1.0 tzdata --system
       - name: install-reqs
@@ -41,8 +45,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install-minimum-versions
         run: uv pip install tox virtualenv setuptools pandas==1.1.5 polars==0.20.3 numpy==1.17.5 pyarrow==11.0.0 scipy==1.5.0 scikit-learn==1.1.0 tzdata --system
       - name: install-reqs
@@ -66,8 +74,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install-minimum-versions
         run: uv pip install tox virtualenv setuptools pandas==2.0.3 polars==0.20.8 numpy==1.24.4 pyarrow==14.0.0 scipy==1.8.0 scikit-learn==1.3.0 dask[dataframe]==2024.7 tzdata --system
       - name: install-reqs
@@ -79,22 +91,34 @@ jobs:
       - name: Run doctests
         run: pytest narwhals --doctest-modules
 
-  pandas-nightly-and-dask:
+  nightlies:
     strategy:
       matrix:
-        python-version: ["3.12"]
+        python-version: ["3.11"]
         os: [ubuntu-latest]
-
+    if: github.event.pull_request.head.repo.full_name == github.repository
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
+      - name: install-kaggle
+        run: uv pip install kaggle --system
+      - name: Download Kaggle notebook artifact
+        env:
+          KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }}
+          KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }}
+        run: |
+          kaggle kernels output "marcogorelli/variable-brink-glacier"
       - name: install-polars
-        run: uv pip install polars --system
+        run: python -m pip install *.whl
       - name: install-reqs
         run: uv pip install --upgrade tox virtualenv setuptools pip -r requirements-dev.txt --system
       - name: uninstall pyarrow
@@ -119,43 +143,4 @@ jobs:
       - name: Run doctests
         run: pytest narwhals --doctest-modules
 
-  # polars-nightly:
-  #   if: github.ref == 'refs/heads/main'
-  #   strategy:
-  #     matrix:
-  #       python-version: ["3.12"]
-  #       os: [ubuntu-latest]
 
-  #   runs-on: ${{ matrix.os }}
-  #   steps:
-  #     - uses: actions/checkout@v4
-  #     - uses: actions/setup-python@v5
-  #       with:
-  #         python-version: ${{ matrix.python-version }}
-  #     - name: Cache multiple paths
-  #       uses: actions/cache@v4
-  #       with:
-  #         path: |
-  #           ~/.cache/pip
-  #           $RUNNER_TOOL_CACHE/Python/*
-  #           ~\AppData\Local\pip\Cache
-  #         key: ${{ runner.os }}-build-${{ matrix.python-version }}
-  #     - name: install-kaggle
-  #       run: python -m pip install kaggle
-  #     - name: Download Kaggle notebook artifact
-  #       env:
-  #         KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }}
-  #         KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }}
-  #       run: kaggle kernels output marcogorelli/polars-nightly
-  #     - name: install-reqs
-  #       run: python -m pip install --upgrade tox virtualenv setuptools pip -r requirements-dev.txt
-  #     - name: uninstall polars
-  #       run: python -m pip uninstall polars -y
-  #     - name: install-modin-pandas
-  #       run: pip install modin[dask] pandas
-  #     - name: install-polars-nightly
-  #       run: python -m pip install *.whl
-  #     - name: Run pytest
-  #       run: pytest tests --cov=narwhals --cov=tests --cov-fail-under=50
-  #     - name: Run doctests
-  #       run: pytest narwhals --doctest-modules
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f3a68e7a..04e41ea3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,7 @@
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
   # Ruff version.
-  rev: 'v0.6.3'
+  rev: 'v0.6.4'
   hooks:
     # Run the formatter.
     - id: ruff-format
diff --git a/README.md b/README.md
index d26107e6..29623920 100644
--- a/README.md
+++ b/README.md
@@ -95,9 +95,6 @@ See the [tutorial](https://narwhals-dev.github.io/narwhals/basics/dataframe/) fo
 
 If you said yes to both, we'd love to hear from you!
 
-**Note**: You might suspect that this is a secret ploy to infiltrate the Polars API everywhere.
-Indeed, you may suspect that.
-
 ## Sponsors and institutional partners
 
 Narwhals is 100% independent, community-driven, and community-owned.
@@ -113,6 +110,31 @@ provided some funding / development time:
 If you contribute to Narwhals on your organization's time, please let us know. We'd be happy to add your employer
 to this list!
 
+## Appears on
+
+Narwhals has been featured in several talks, podcasts, and blog posts:
+
+- [Talk Python to me Podcast](https://youtu.be/FSH7BZ0tuE0)
+  Ahoy, Narwhals are bridging the data science APIs
+
+- [Super Data Science: ML & AI Podcast](https://www.youtube.com/watch?v=TeG4U8R0U8U)  
+  Narwhals: For Pandas-to-Polars DataFrame Compatibility
+
+- [Sample Space Podcast | probabl](https://youtu.be/8hYdq4sWbbQ?si=WG0QP1CZ6gkFf18b)  
+  How Narwhals has many end users ... that never use it directly. - Marco Gorelli
+
+- [Pycon Lithuania](https://www.youtube.com/watch?v=-mdx7Cn6_6E)  
+  Marco Gorelli - DataFrame interoperatiblity - what's been achieved, and what comes next?
+
+- [Pycon Italy](https://www.youtube.com/watch?v=3IqUli9XsmQ)  
+  How you can write a dataframe-agnostic library - Marco Gorelli
+
+- [Polars Blog Post](https://pola.rs/posts/lightweight_plotting/)  
+  Polars has a new lightweight plotting backend
+
+- [Quansight Labs blog post (w/ Scikit-Lego)](https://labs.quansight.org/blog/scikit-lego-narwhals)  
+  How Narwhals and scikit-lego came together to achieve dataframe-agnosticism
+
 ## Why "Narwhals"?
 
 [Coz they are so awesome](https://youtu.be/ykwqXuMPsoc?si=A-i8LdR38teYsos4).
diff --git a/docs/api-reference/dataframe.md b/docs/api-reference/dataframe.md
index f78b4e3d..fe0c4025 100644
--- a/docs/api-reference/dataframe.md
+++ b/docs/api-reference/dataframe.md
@@ -29,6 +29,7 @@
         - rename
         - row
         - rows
+        - sample
         - schema
         - select
         - shape
diff --git a/docs/api-reference/expr.md b/docs/api-reference/expr.md
index cc1290a8..7188b2c3 100644
--- a/docs/api-reference/expr.md
+++ b/docs/api-reference/expr.md
@@ -30,6 +30,7 @@
         - max
         - mean
         - min
+        - mode
         - null_count
         - n_unique
         - over
diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md
index f9cc2e6b..c016b566 100644
--- a/docs/api-reference/series.md
+++ b/docs/api-reference/series.md
@@ -12,6 +12,7 @@
         - any
         - arg_true
         - cast
+        - clip
         - count
         - cum_sum
         - diff
@@ -22,7 +23,6 @@
         - gather_every
         - head
         - is_between
-        - clip
         - is_duplicated
         - is_empty
         - is_first_distinct
@@ -36,13 +36,15 @@
         - max
         - mean
         - min
+        - mode
         - name
-        - null_count
         - n_unique
+        - null_count
         - pipe
         - quantile
         - round
         - sample
+        - scatter
         - shape
         - shift
         - sort
diff --git a/docs/basics/complete_example.md b/docs/basics/complete_example.md
index 1e2cfe30..d3b4ecfe 100644
--- a/docs/basics/complete_example.md
+++ b/docs/basics/complete_example.md
@@ -10,10 +10,6 @@ We'll need to write two methods:
 - `transform`: scale a given dataset with the mean and standard deviations calculated
   during `fit`.
 
-The `fit` method is a bit complicated, so let's start with `transform`.
-Suppose we've already calculated the mean and standard deviation of each column, and have
-stored them in attributes `self.means` and `self.std_devs`.
-
 ## Fit method
 
 Unlike the `transform` method, which we'll write below, `fit` cannot stay lazy,
diff --git a/docs/installation.md b/docs/installation.md
index 58302d41..61760681 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -11,6 +11,6 @@ Then, if you start the Python REPL and see the following:
 ```python
 >>> import narwhals
 >>> narwhals.__version__
-'1.6.4'
+'1.8.1'
 ```
 then installation worked correctly!
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index f410a1b2..3c8b7b77 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -53,7 +53,7 @@ from narwhals.utils import maybe_convert_dtypes
 from narwhals.utils import maybe_get_index
 from narwhals.utils import maybe_set_index
 
-__version__ = "1.6.4"
+__version__ = "1.8.1"
 
 __all__ = [
     "dependencies",
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index fa5a6995..428e83e3 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -9,6 +9,7 @@ from typing import Sequence
 from typing import overload
 
 from narwhals._arrow.utils import broadcast_series
+from narwhals._arrow.utils import convert_slice_to_nparray
 from narwhals._arrow.utils import translate_dtype
 from narwhals._arrow.utils import validate_dataframe_comparand
 from narwhals._expression_parsing import evaluate_into_exprs
@@ -121,7 +122,13 @@ class ArrowDataFrame:
     def __getitem__(self, item: slice) -> ArrowDataFrame: ...
 
     def __getitem__(
-        self, item: str | slice | Sequence[int] | tuple[Sequence[int], str | int]
+        self,
+        item: str
+        | slice
+        | Sequence[int]
+        | Sequence[str]
+        | tuple[Sequence[int], str | int]
+        | tuple[slice, str | int],
     ) -> ArrowSeries | ArrowDataFrame:
         if isinstance(item, str):
             from narwhals._arrow.series import ArrowSeries
@@ -139,7 +146,10 @@ class ArrowDataFrame:
             if item[0] == slice(None):
                 selected_rows = self._native_frame
             else:
-                selected_rows = self._native_frame.take(item[0])
+                range_ = convert_slice_to_nparray(
+                    num_rows=len(self._native_frame), rows_slice=item[0]
+                )
+                selected_rows = self._native_frame.take(range_)
 
             return self._from_native_frame(selected_rows.select(item[1]))
 
@@ -169,13 +179,24 @@ class ArrowDataFrame:
                     )
                 msg = f"Expected slice of integers or strings, got: {type(item[1])}"  # pragma: no cover
                 raise TypeError(msg)  # pragma: no cover
-
             from narwhals._arrow.series import ArrowSeries
 
             # PyArrow columns are always strings
             col_name = item[1] if isinstance(item[1], str) else self.columns[item[1]]
+            if isinstance(item[0], str):  # pragma: no cover
+                msg = "Can not slice with tuple with the first element as a str"
+                raise TypeError(msg)
+            if (isinstance(item[0], slice)) and (item[0] == slice(None)):
+                return ArrowSeries(
+                    self._native_frame[col_name],
+                    name=col_name,
+                    backend_version=self._backend_version,
+                )
+            range_ = convert_slice_to_nparray(
+                num_rows=len(self._native_frame), rows_slice=item[0]
+            )
             return ArrowSeries(
-                self._native_frame[col_name].take(item[0]),
+                self._native_frame[col_name].take(range_),
                 name=col_name,
                 backend_version=self._backend_version,
             )
@@ -191,6 +212,8 @@ class ArrowDataFrame:
             )
 
         elif isinstance(item, Sequence) or (is_numpy_array(item) and item.ndim == 1):
+            if isinstance(item, Sequence) and all(isinstance(x, str) for x in item):
+                return self._from_native_frame(self._native_frame.select(item))
             return self._from_native_frame(self._native_frame.take(item))
 
         else:  # pragma: no cover
@@ -565,3 +588,25 @@ class ArrowDataFrame:
 
     def to_arrow(self: Self) -> Any:
         return self._native_frame
+
+    def sample(
+        self: Self,
+        n: int | None = None,
+        *,
+        fraction: float | None = None,
+        with_replacement: bool = False,
+        seed: int | None = None,
+    ) -> Self:
+        import numpy as np  # ignore-banned-import
+        import pyarrow.compute as pc  # ignore-banned-import()
+
+        frame = self._native_frame
+        num_rows = len(self)
+        if n is None and fraction is not None:
+            n = int(num_rows * fraction)
+
+        rng = np.random.default_rng(seed=seed)
+        idx = np.arange(0, num_rows)
+        mask = rng.choice(idx, size=n, replace=with_replacement)
+
+        return self._from_native_frame(pc.take(frame, mask))
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 31052fa5..b324be2b 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -249,12 +249,18 @@ class ArrowExpr:
     def sample(
         self: Self,
         n: int | None = None,
-        fraction: float | None = None,
         *,
+        fraction: float | None = None,
         with_replacement: bool = False,
+        seed: int | None = None,
     ) -> Self:
         return reuse_series_implementation(
-            self, "sample", n=n, fraction=fraction, with_replacement=with_replacement
+            self,
+            "sample",
+            n=n,
+            fraction=fraction,
+            with_replacement=with_replacement,
+            seed=seed,
         )
 
     def fill_null(self: Self, value: Any) -> Self:
@@ -323,6 +329,9 @@ class ArrowExpr:
             backend_version=self._backend_version,
         )
 
+    def mode(self: Self) -> Self:
+        return reuse_series_implementation(self, "mode")
+
     @property
     def dt(self: Self) -> ArrowExprDateTimeNamespace:
         return ArrowExprDateTimeNamespace(self)
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 9d3d690d..4f53a3f0 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
     from typing_extensions import Self
 
     from narwhals._arrow.dataframe import ArrowDataFrame
+    from narwhals._arrow.namespace import ArrowNamespace
     from narwhals.dtypes import DType
 
 
@@ -65,6 +66,11 @@ class ArrowSeries:
             backend_version=backend_version,
         )
 
+    def __narwhals_namespace__(self) -> ArrowNamespace:
+        from narwhals._arrow.namespace import ArrowNamespace
+
+        return ArrowNamespace(backend_version=self._backend_version)
+
     def __len__(self) -> int:
         return len(self._native_series)
 
@@ -315,8 +321,27 @@ class ArrowSeries:
     def __getitem__(self, idx: int | slice | Sequence[int]) -> Any | Self:
         if isinstance(idx, int):
             return self._native_series[idx]
+        if isinstance(idx, Sequence):
+            return self._from_native_series(self._native_series.take(idx))
         return self._from_native_series(self._native_series[idx])
 
+    def scatter(self, indices: int | Sequence[int], values: Any) -> Self:
+        import numpy as np  # ignore-banned-import
+        import pyarrow as pa  # ignore-banned-import
+        import pyarrow.compute as pc  # ignore-banned-import
+
+        ca = self._native_series
+        mask = np.zeros(len(ca), dtype=bool)
+        mask[indices] = True
+        if isinstance(values, self.__class__):
+            values = validate_column_comparand(values)
+        if isinstance(values, pa.ChunkedArray):
+            values = values.combine_chunks()
+        if not isinstance(values, pa.Array):
+            values = pa.array(values)
+        result = pc.replace_with_mask(ca, mask, values.take(indices))
+        return self._from_native_series(result)
+
     def to_list(self) -> Any:
         return self._native_series.to_pylist()
 
@@ -509,9 +534,10 @@ class ArrowSeries:
     def sample(
         self: Self,
         n: int | None = None,
-        fraction: float | None = None,
         *,
+        fraction: float | None = None,
         with_replacement: bool = False,
+        seed: int | None = None,
     ) -> Self:
         import numpy as np  # ignore-banned-import
         import pyarrow.compute as pc  # ignore-banned-import()
@@ -522,8 +548,10 @@ class ArrowSeries:
         if n is None and fraction is not None:
             n = int(num_rows * fraction)
 
+        rng = np.random.default_rng(seed=seed)
         idx = np.arange(0, num_rows)
-        mask = np.random.choice(idx, size=n, replace=with_replacement)
+        mask = rng.choice(idx, size=n, replace=with_replacement)
+
         return self._from_native_series(pc.take(ser, mask))
 
     def fill_null(self: Self, value: Any) -> Self:
@@ -667,6 +695,13 @@ class ArrowSeries:
     def to_arrow(self: Self) -> pa.Array:
         return self._native_series.combine_chunks()
 
+    def mode(self: Self) -> ArrowSeries:
+        plx = self.__narwhals_namespace__()
+        col_token = generate_unique_token(n_bytes=8, columns=[self.name])
+        return self.value_counts(name=col_token, normalize=False).filter(
+            plx.col(col_token) == plx.col(col_token).max()
+        )[self.name]
+
     @property
     def shape(self) -> tuple[int]:
         return (len(self._native_series),)
@@ -896,6 +931,7 @@ class ArrowSeriesCatNamespace:
 
         ca = self._arrow_series._native_series
         # TODO(Unassigned): this looks potentially expensive - is there no better way?
+        # https://github.com/narwhals-dev/narwhals/issues/464
         out = pa.chunked_array(
             [pa.concat_arrays([x.dictionary for x in ca.chunks]).unique()]
         )
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index b8294839..a2a45586 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 from typing import Any
+from typing import Sequence
 
 from narwhals import dtypes
 from narwhals.utils import isinstance_or_issubclass
@@ -11,7 +12,7 @@ if TYPE_CHECKING:
 
 
 def translate_dtype(dtype: Any) -> dtypes.DType:
-    import pyarrow as pa  # ignore-banned-import()
+    import pyarrow as pa  # ignore-banned-import
 
     if pa.types.is_int64(dtype):
         return dtypes.Int64()
@@ -55,7 +56,7 @@ def translate_dtype(dtype: Any) -> dtypes.DType:
 
 
 def narwhals_to_native_dtype(dtype: dtypes.DType | type[dtypes.DType]) -> Any:
-    import pyarrow as pa  # ignore-banned-import()
+    import pyarrow as pa  # ignore-banned-import
 
     from narwhals import dtypes
 
@@ -84,8 +85,6 @@ def narwhals_to_native_dtype(dtype: dtypes.DType | type[dtypes.DType]) -> Any:
     if isinstance_or_issubclass(dtype, dtypes.Boolean):
         return pa.bool_()
     if isinstance_or_issubclass(dtype, dtypes.Categorical):
-        # TODO(Unassigned): what should the key be? let's keep it consistent
-        # with Polars for now
         return pa.dictionary(pa.uint32(), pa.string())
     if isinstance_or_issubclass(dtype, dtypes.Datetime):
         # Use Polars' default
@@ -142,7 +141,7 @@ def validate_dataframe_comparand(
         return NotImplemented
     if isinstance(other, ArrowSeries):
         if len(other) == 1:
-            import pyarrow as pa  # ignore-banned-import()
+            import pyarrow as pa  # ignore-banned-import
 
             value = other.item()
             if backend_version < (13,) and hasattr(value, "as_py"):  # pragma: no cover
@@ -159,7 +158,7 @@ def horizontal_concat(dfs: list[Any]) -> Any:
 
     Should be in namespace.
     """
-    import pyarrow as pa  # ignore-banned-import()
+    import pyarrow as pa  # ignore-banned-import
 
     if not dfs:
         msg = "No dataframes to concatenate"  # pragma: no cover
@@ -192,7 +191,7 @@ def vertical_concat(dfs: list[Any]) -> Any:
             msg = "unable to vstack, column names don't match"
             raise TypeError(msg)
 
-    import pyarrow as pa  # ignore-banned-import()
+    import pyarrow as pa  # ignore-banned-import
 
     return pa.concat_tables(dfs).combine_chunks()
 
@@ -200,8 +199,8 @@ def vertical_concat(dfs: list[Any]) -> Any:
 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
-    import pyarrow as pa  # ignore-banned-import()
-    import pyarrow.compute as pc  # ignore-banned-import()
+    import pyarrow as pa  # ignore-banned-import
+    import pyarrow.compute as pc  # ignore-banned-import
 
     if isinstance(left, (int, float)):
         left = pa.scalar(left)
@@ -239,8 +238,8 @@ def floordiv_compat(left: Any, right: Any) -> Any:
 def cast_for_truediv(arrow_array: Any, pa_object: Any) -> tuple[Any, Any]:
     # Lifted from:
     # https://github.com/pandas-dev/pandas/blob/262fcfbffcee5c3116e86a951d8b693f90411e68/pandas/core/arrays/arrow/array.py#L108-L122
-    import pyarrow as pa  # ignore-banned-import()
-    import pyarrow.compute as pc  # ignore-banned-import()
+    import pyarrow as pa  # ignore-banned-import
+    import pyarrow.compute as pc  # ignore-banned-import
 
     # Ensure int / int -> float mirroring Python/Numpy behavior
     # as pc.divide_checked(int, int) -> int
@@ -262,7 +261,7 @@ def broadcast_series(series: list[ArrowSeries]) -> list[Any]:
     if fast_path:
         return [s._native_series for s in series]
 
-    import pyarrow as pa  # ignore-banned-import()
+    import pyarrow as pa  # ignore-banned-import
 
     reshaped = []
     for s, length in zip(series, lengths):
@@ -276,3 +275,14 @@ def broadcast_series(series: list[ArrowSeries]) -> list[Any]:
             reshaped.append(s_native)
 
     return reshaped
+
+
+def convert_slice_to_nparray(
+    num_rows: int, rows_slice: slice | int | Sequence[int]
+) -> Any:
+    import numpy as np  # ignore-banned-import
+
+    if isinstance(rows_slice, slice):
+        return np.arange(num_rows)[rows_slice]
+    else:
+        return rows_slice
diff --git a/narwhals/_dask/dataframe.py b/narwhals/_dask/dataframe.py
index e2a034ae..ac10ac2b 100644
--- a/narwhals/_dask/dataframe.py
+++ b/narwhals/_dask/dataframe.py
@@ -79,14 +79,17 @@ class DaskLazyFrame:
             and isinstance(predicates[0], list)
             and all(isinstance(x, bool) for x in predicates[0])
         ):
-            mask = predicates[0]
-        else:
-            from narwhals._dask.namespace import DaskNamespace
+            msg = (
+                "`LazyFrame.filter` is not supported for Dask backend with boolean masks."
+            )
+            raise NotImplementedError(msg)
+
+        from narwhals._dask.namespace import DaskNamespace
 
-            plx = DaskNamespace(backend_version=self._backend_version)
-            expr = plx.all_horizontal(*predicates)
-            # Safety: all_horizontal's expression only returns a single column.
-            mask = expr._call(self)[0]
+        plx = DaskNamespace(backend_version=self._backend_version)
+        expr = plx.all_horizontal(*predicates)
+        # Safety: all_horizontal's expression only returns a single column.
+        mask = expr._call(self)[0]
         return self._from_native_frame(self._native_frame.loc[mask])
 
     def lazy(self) -> Self:
@@ -210,6 +213,10 @@ class DaskLazyFrame:
         right_on: str | list[str] | None,
         suffix: str,
     ) -> Self:
+        if isinstance(left_on, str):
+            left_on = [left_on]
+        if isinstance(right_on, str):
+            right_on = [right_on]
         if how == "cross":
             key_token = generate_unique_token(
                 n_bytes=8, columns=[*self.columns, *other.columns]
@@ -328,7 +335,14 @@ class DaskLazyFrame:
         return DaskLazyGroupBy(self, list(by))
 
     def tail(self: Self, n: int) -> Self:
-        return self._from_native_frame(self._native_frame.tail(n=n, compute=False))
+        native_frame = self._native_frame
+        n_partitions = native_frame.npartitions
+
+        if n_partitions == 1:
+            return self._from_native_frame(self._native_frame.tail(n=n, compute=False))
+        else:
+            msg = "`LazyFrame.tail` is not supported for Dask backend with multiple partitions."
+            raise NotImplementedError(msg)
 
     def gather_every(self: Self, n: int, offset: int) -> Self:
         row_index_token = generate_unique_token(n_bytes=8, columns=self.columns)
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index 73082450..bb5f8ddc 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -103,13 +103,11 @@ class DaskExpr:
             if root_names is not None and isinstance(arg, self.__class__):
                 if arg._root_names is not None:
                     root_names.extend(arg._root_names)
-                else:  # pragma: no cover
-                    # TODO(unassigned): increase coverage
+                else:
                     root_names = None
                     output_names = None
                     break
-            elif root_names is None:  # pragma: no cover
-                # TODO(unassigned): increase coverage
+            elif root_names is None:
                 output_names = None
                 break
 
@@ -431,6 +429,11 @@ class DaskExpr:
             returns_scalar=False,
         )
 
+    def unique(self) -> NoReturn:
+        # We can't (yet?) allow methods which modify the index
+        msg = "`Expr.unique` is not supported for the Dask backend. Please use `LazyFrame.unique` instead."
+        raise NotImplementedError(msg)
+
     def drop_nulls(self) -> NoReturn:
         # We can't (yet?) allow methods which modify the index
         msg = "`Expr.drop_nulls` is not supported for the Dask backend. Please use `LazyFrame.drop_nulls` instead."
@@ -524,8 +527,15 @@ class DaskExpr:
         interpolation: Literal["nearest", "higher", "lower", "midpoint", "linear"],
     ) -> Self:
         if interpolation == "linear":
+
+            def func(_input: dask_expr.Series, _quantile: float) -> dask_expr.Series:
+                if _input.npartitions > 1:
+                    msg = "`Expr.quantile` is not supported for Dask backend with multiple partitions."
+                    raise NotImplementedError(msg)
+                return _input.quantile(q=_quantile, method="dask")
+
             return self._from_call(
-                lambda _input, quantile: _input.quantile(q=quantile, method="dask"),
+                func,
                 "quantile",
                 quantile,
                 returns_scalar=True,
@@ -626,6 +636,11 @@ class DaskExpr:
                     "`nw.col('a', 'b')`\n"
                 )
                 raise ValueError(msg)
+
+            if df._native_frame.npartitions > 1:
+                msg = "`Expr.over` is not supported for Dask backend with multiple partitions."
+                raise NotImplementedError(msg)
+
             tmp = df.group_by(*keys).agg(self)
             tmp_native = (
                 df.select(*keys)
@@ -644,6 +659,10 @@ class DaskExpr:
             backend_version=self._backend_version,
         )
 
+    def mode(self: Self) -> Self:
+        msg = "`Expr.mode` is not supported for the Dask backend."
+        raise NotImplementedError(msg)
+
     @property
     def str(self: Self) -> DaskExprStringNamespace:
         return DaskExprStringNamespace(self)
diff --git a/narwhals/_dask/group_by.py b/narwhals/_dask/group_by.py
index 463d6fc5..d5fbaaf9 100644
--- a/narwhals/_dask/group_by.py
+++ b/narwhals/_dask/group_by.py
@@ -72,6 +72,7 @@ class DaskLazyGroupBy:
             output_names.extend(expr._output_names)
 
         return agg_dask(
+            self._df,
             self._grouped,
             exprs,
             self._keys,
@@ -88,6 +89,7 @@ class DaskLazyGroupBy:
 
 
 def agg_dask(
+    df: DaskLazyFrame,
     grouped: Any,
     exprs: list[DaskExpr],
     keys: list[str],
@@ -99,6 +101,10 @@ def agg_dask(
     - https://github.com/rapidsai/cudf/issues/15118
     - https://github.com/rapidsai/cudf/issues/15084
     """
+    if not exprs:
+        # No aggregation provided
+        return df.select(*keys).unique(subset=keys)
+
     all_simple_aggs = True
     for expr in exprs:
         if not is_simple_aggregation(expr):
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 59cff49f..62067069 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -111,13 +111,22 @@ class PandasLikeDataFrame:
     def __getitem__(self, item: Sequence[int]) -> PandasLikeDataFrame: ...
 
     @overload
-    def __getitem__(self, item: str) -> PandasLikeSeries: ...
+    def __getitem__(self, item: str) -> PandasLikeSeries: ...  # type: ignore[overload-overlap]
+
+    @overload
+    def __getitem__(self, item: Sequence[str]) -> PandasLikeDataFrame: ...
 
     @overload
     def __getitem__(self, item: slice) -> PandasLikeDataFrame: ...
 
     def __getitem__(
-        self, item: str | slice | Sequence[int] | tuple[Sequence[int], str | int]
+        self,
+        item: str
+        | int
+        | slice
+        | Sequence[int]
+        | Sequence[str]
+        | tuple[Sequence[int], str | int],
     ) -> PandasLikeSeries | PandasLikeDataFrame:
         if isinstance(item, str):
             from narwhals._pandas_like.series import PandasLikeSeries
@@ -174,7 +183,7 @@ class PandasLikeDataFrame:
             from narwhals._pandas_like.series import PandasLikeSeries
 
             if isinstance(item[1], str):
-                item = (item[0], self._native_frame.columns.get_loc(item[1]))
+                item = (item[0], self._native_frame.columns.get_loc(item[1]))  # type: ignore[assignment]
                 native_series = self._native_frame.iloc[item]
             elif isinstance(item[1], int):
                 native_series = self._native_frame.iloc[item]
@@ -191,6 +200,8 @@ class PandasLikeDataFrame:
         elif isinstance(item, (slice, Sequence)) or (
             is_numpy_array(item) and item.ndim == 1
         ):
+            if isinstance(item, Sequence) and all(isinstance(x, str) for x in item):
+                return self._from_native_frame(self._native_frame.loc[:, item])
             return self._from_native_frame(self._native_frame.iloc[item])
 
         else:  # pragma: no cover
@@ -590,8 +601,8 @@ class PandasLikeDataFrame:
         from narwhals._pandas_like.series import PANDAS_TO_NUMPY_DTYPE_MISSING
 
         if copy is None:
-            # pandas default differs from Polars
-            copy = False
+            # pandas default differs from Polars, but cuDF default is True
+            copy = self._implementation is Implementation.CUDF
 
         if dtype is not None:
             return self._native_frame.to_numpy(dtype=dtype, copy=copy)
@@ -682,3 +693,17 @@ class PandasLikeDataFrame:
         import pyarrow as pa  # ignore-banned-import()
 
         return pa.Table.from_pandas(self._native_frame)
+
+    def sample(
+        self: Self,
+        n: int | None = None,
+        *,
+        fraction: float | None = None,
+        with_replacement: bool = False,
+        seed: int | None = None,
+    ) -> Self:
+        return self._from_native_frame(
+            self._native_frame.sample(
+                n=n, frac=fraction, replace=with_replacement, random_state=seed
+            )
+        )
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index 8c3536c7..409d1ab0 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -254,14 +254,20 @@ class PandasLikeExpr:
         return reuse_series_implementation(self, "shift", n=n)
 
     def sample(
-        self,
+        self: Self,
         n: int | None = None,
-        fraction: float | None = None,
         *,
+        fraction: float | None = None,
         with_replacement: bool = False,
+        seed: int | None = None,
     ) -> Self:
         return reuse_series_implementation(
-            self, "sample", n=n, fraction=fraction, with_replacement=with_replacement
+            self,
+            "sample",
+            n=n,
+            fraction=fraction,
+            with_replacement=with_replacement,
+            seed=seed,
         )
 
     def alias(self, name: str) -> Self:
@@ -338,6 +344,9 @@ class PandasLikeExpr:
     def gather_every(self: Self, n: int, offset: int = 0) -> Self:
         return reuse_series_implementation(self, "gather_every", n=n, offset=offset)
 
+    def mode(self: Self) -> Self:
+        return reuse_series_implementation(self, "mode")
+
     @property
     def str(self: Self) -> PandasLikeExprStringNamespace:
         return PandasLikeExprStringNamespace(self)
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 97a477dc..892291d5 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -80,6 +80,7 @@ class PandasLikeGroupBy:
             dataframe_is_empty=self._df._native_frame.empty,
             implementation=implementation,
             backend_version=self._df._backend_version,
+            native_namespace=self._df.__native_namespace__(),
         )
 
     def _from_native_frame(self, df: PandasLikeDataFrame) -> PandasLikeDataFrame:
@@ -114,6 +115,7 @@ def agg_pandas(  # noqa: PLR0915
     implementation: Any,
     backend_version: tuple[int, ...],
     dataframe_is_empty: bool,
+    native_namespace: Any,
 ) -> PandasLikeDataFrame:
     """
     This should be the fastpath, but cuDF is too far behind to use it.
@@ -204,9 +206,9 @@ def agg_pandas(  # noqa: PLR0915
             result_aggs = result_nunique_aggs
         elif simple_aggs and not nunique_aggs:
             result_aggs = result_simple_aggs
-        else:  # pragma: no cover
-            msg = "Congrats, you entered unreachable code. Please report a bug to https://github.com/narwhals-dev/narwhals/issues."
-            raise RuntimeError(msg)
+        else:
+            # No aggregation provided
+            result_aggs = native_namespace.DataFrame(grouped.groups.keys(), columns=keys)
         return from_dataframe(result_aggs.loc[:, output_names])
 
     if dataframe_is_empty:
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index b28a0408..14d7a128 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -10,6 +10,7 @@ from typing import overload
 from narwhals._pandas_like.utils import int_dtype_mapper
 from narwhals._pandas_like.utils import narwhals_to_native_dtype
 from narwhals._pandas_like.utils import native_series_from_iterable
+from narwhals._pandas_like.utils import set_axis
 from narwhals._pandas_like.utils import to_datetime
 from narwhals._pandas_like.utils import translate_dtype
 from narwhals._pandas_like.utils import validate_column_comparand
@@ -167,6 +168,22 @@ class PandasLikeSeries:
     def dtype(self) -> DType:
         return translate_dtype(self._native_series)
 
+    def scatter(self, indices: int | Sequence[int], values: Any) -> Self:
+        if isinstance(values, self.__class__):
+            # .copy() is necessary in some pre-2.2 versions of pandas to avoid
+            # `values` also getting modified (!)
+            values = validate_column_comparand(self._native_series.index, values).copy()
+            values = set_axis(
+                values,
+                self._native_series.index[indices],
+                implementation=self._implementation,
+                backend_version=self._backend_version,
+            )
+        s = self._native_series
+        s.iloc[indices] = values
+        s.name = self.name
+        return self._from_native_series(s)
+
     def cast(
         self,
         dtype: Any,
@@ -419,15 +436,16 @@ class PandasLikeSeries:
         return ser.nunique(dropna=False)  # type: ignore[no-any-return]
 
     def sample(
-        self,
+        self: Self,
         n: int | None = None,
-        fraction: float | None = None,
         *,
+        fraction: float | None = None,
         with_replacement: bool = False,
-    ) -> PandasLikeSeries:
+        seed: int | None = None,
+    ) -> Self:
         ser = self._native_series
         return self._from_native_series(
-            ser.sample(n=n, frac=fraction, replace=with_replacement)
+            ser.sample(n=n, frac=fraction, replace=with_replacement, random_state=seed)
         )
 
     def abs(self) -> PandasLikeSeries:
@@ -473,7 +491,7 @@ class PandasLikeSeries:
     def to_numpy(self, dtype: Any = None, copy: bool | None = None) -> Any:
         # the default is meant to be None, but pandas doesn't allow it?
         # https://numpy.org/doc/stable/reference/generated/numpy.ndarray.__array__.html
-        copy = copy or False
+        copy = copy or self._implementation is Implementation.CUDF
 
         has_missing = self._native_series.isna().any()
         if (
@@ -641,6 +659,12 @@ class PandasLikeSeries:
 
         return pa.Array.from_pandas(self._native_series)
 
+    def mode(self: Self) -> Self:
+        native_series = self._native_series
+        result = native_series.mode()
+        result.name = native_series.name
+        return self._from_native_series(result)
+
     @property
     def str(self) -> PandasLikeSeriesStringNamespace:
         return PandasLikeSeriesStringNamespace(self)
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index a266b73c..b51d53ba 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -602,17 +602,24 @@ class DataFrame(BaseFrame[FrameT]):
     @overload
     def __getitem__(self, item: tuple[Sequence[int], str]) -> Series: ...  # type: ignore[overload-overlap]
     @overload
+    def __getitem__(self, item: tuple[slice, str]) -> Series: ...  # type: ignore[overload-overlap]
+    @overload
     def __getitem__(self, item: tuple[Sequence[int], Sequence[str]]) -> Self: ...
     @overload
     def __getitem__(self, item: tuple[slice, Sequence[str]]) -> Self: ...
     @overload
     def __getitem__(self, item: tuple[Sequence[int], int]) -> Series: ...  # type: ignore[overload-overlap]
+    @overload
+    def __getitem__(self, item: tuple[slice, int]) -> Series: ...  # type: ignore[overload-overlap]
 
     @overload
     def __getitem__(self, item: Sequence[int]) -> Self: ...
 
     @overload
-    def __getitem__(self, item: str) -> Series: ...
+    def __getitem__(self, item: str) -> Series: ...  # type: ignore[overload-overlap]
+
+    @overload
+    def __getitem__(self, item: Sequence[str]) -> Self: ...
 
     @overload
     def __getitem__(self, item: slice) -> Self: ...
@@ -622,7 +629,9 @@ class DataFrame(BaseFrame[FrameT]):
         item: str
         | slice
         | Sequence[int]
+        | Sequence[str]
         | tuple[Sequence[int], str | int]
+        | tuple[slice, str | int]
         | tuple[slice | Sequence[int], Sequence[int] | Sequence[str] | slice],
     ) -> Series | Self:
         """
@@ -644,6 +653,8 @@ class DataFrame(BaseFrame[FrameT]):
                   `DataFrame`.
                 - `df[:, ['a', 'c']]` extracts all rows and columns `'a'` and `'c'` and returns a
                   `DataFrame`.
+                - `df[['a', 'c']]` extracts all rows and columns `'a'` and `'c'` and returns a
+                  `DataFrame`.
                 - `df[0: 2, ['a', 'c']]` extracts the first two rows and columns `'a'` and `'c'` and
                     returns a `DataFrame`
                 - `df[:, 0: 2]` extracts all rows from the first two columns and returns a `DataFrame`
@@ -795,6 +806,9 @@ class DataFrame(BaseFrame[FrameT]):
         Arguments:
             index: Row number.
 
+        Notes:
+            cuDF doesn't support this method.
+
         Examples:
             >>> import narwhals as nw
             >>> import pandas as pd
@@ -1131,6 +1145,9 @@ class DataFrame(BaseFrame[FrameT]):
                 internally while iterating over the data.
                 See https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.iter_rows.html
 
+        Notes:
+            cuDF doesn't support this method.
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -2447,6 +2464,66 @@ class DataFrame(BaseFrame[FrameT]):
         """
         return self._compliant_frame.to_arrow()
 
+    def sample(
+        self: Self,
+        n: int | None = None,
+        *,
+        fraction: float | None = None,
+        with_replacement: bool = False,
+        seed: int | None = None,
+    ) -> Self:
+        r"""
+        Sample from this DataFrame.
+
+        Arguments:
+            n: Number of items to return. Cannot be used with fraction.
+            fraction: Fraction of items to return. Cannot be used with n.
+            with_replacement: Allow values to be sampled more than once.
+            seed: Seed for the random number generator. If set to None (default), a random
+                seed is generated for each sample operation.
+
+        Notes:
+            The results may not be consistent across libraries.
+
+        Examples:
+            >>> import narwhals as nw
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> data = {"a": [1, 2, 3, 4], "b": ["x", "y", "x", "y"]}
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.DataFrame(data)
+
+            We define a library agnostic function:
+
+            >>> @nw.narwhalify
+            ... def func(df):
+            ...     return df.sample(n=2, seed=123)
+
+            We can then pass either pandas or Polars to `func`:
+            >>> func(df_pd)
+               a  b
+            3  4  y
+            0  1  x
+            >>> func(df_pl)
+            shape: (2, 2)
+            ┌─────┬─────┐
+            │ a   ┆ b   │
+            │ --- ┆ --- │
+            │ i64 ┆ str │
+            ╞═════╪═════╡
+            │ 2   ┆ y   │
+            │ 3   ┆ x   │
+            └─────┴─────┘
+
+            As you can see, by using the same seed, the result will be consistent within
+            the same backend, but not necessarely across different backends.
+        """
+        return self._from_compliant_dataframe(
+            self._compliant_frame.sample(
+                n=n, fraction=fraction, with_replacement=with_replacement, seed=seed
+            )
+        )
+
 
 class LazyFrame(BaseFrame[FrameT]):
     """
@@ -3845,34 +3922,35 @@ class LazyFrame(BaseFrame[FrameT]):
         r"""
         Create a copy of this DataFrame.
 
-        >>> import narwhals as nw
-        >>> import pandas as pd
-        >>> import polars as pl
-        >>> data = {"a": [1, 2], "b": [3, 4]}
-        >>> df_pd = pd.DataFrame(data)
-        >>> df_pl = pl.LazyFrame(data)
-
-        Let's define a dataframe-agnostic function in which we copy the DataFrame:
-
-        >>> @nw.narwhalify
-        ... def func(df):
-        ...     return df.clone()
-
-        >>> func(df_pd)
-           a  b
-        0  1  3
-        1  2  4
-
-        >>> func(df_pl).collect()
-        shape: (2, 2)
-        ┌─────┬─────┐
-        │ a   ┆ b   │
-        │ --- ┆ --- │
-        │ i64 ┆ i64 │
-        ╞═════╪═════╡
-        │ 1   ┆ 3   │
-        │ 2   ┆ 4   │
-        └─────┴─────┘
+        Examples:
+            >>> import narwhals as nw
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> data = {"a": [1, 2], "b": [3, 4]}
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.LazyFrame(data)
+
+            Let's define a dataframe-agnostic function in which we copy the DataFrame:
+
+            >>> @nw.narwhalify
+            ... def func(df):
+            ...     return df.clone()
+
+            >>> func(df_pd)
+               a  b
+            0  1  3
+            1  2  4
+
+            >>> func(df_pl).collect()
+            shape: (2, 2)
+            ┌─────┬─────┐
+            │ a   ┆ b   │
+            │ --- ┆ --- │
+            │ i64 ┆ i64 │
+            ╞═════╪═════╡
+            │ 1   ┆ 3   │
+            │ 2   ┆ 4   │
+            └─────┴─────┘
         """
         return super().clone()
 
diff --git a/narwhals/expr.py b/narwhals/expr.py
index cc8b8695..b39a5981 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -1228,21 +1228,22 @@ class Expr:
         return self.__class__(lambda plx: self._call(plx).drop_nulls())
 
     def sample(
-        self,
+        self: Self,
         n: int | None = None,
-        fraction: float | None = None,
         *,
+        fraction: float | None = None,
         with_replacement: bool = False,
+        seed: int | None = None,
     ) -> Self:
         """
         Sample randomly from this expression.
 
         Arguments:
             n: Number of items to return. Cannot be used with fraction.
-
             fraction: Fraction of items to return. Cannot be used with n.
-
             with_replacement: Allow values to be sampled more than once.
+            seed: Seed for the random number generator. If set to None (default), a random
+                seed is generated for each sample operation.
 
         Examples:
             >>> import narwhals as nw
@@ -1279,7 +1280,7 @@ class Expr:
         """
         return self.__class__(
             lambda plx: self._call(plx).sample(
-                n, fraction=fraction, with_replacement=with_replacement
+                n, fraction=fraction, with_replacement=with_replacement, seed=seed
             )
         )
 
@@ -1585,9 +1586,8 @@ class Expr:
         r"""
         Get the first `n` rows.
 
-        Arguments
-            n : int
-                Number of rows to return.
+        Arguments:
+            n: Number of rows to return.
 
         Examples:
             >>> import narwhals as nw
@@ -1628,9 +1628,8 @@ class Expr:
         r"""
         Get the last `n` rows.
 
-        Arguments
-            n : int
-                Number of rows to return.
+        Arguments:
+            n: Number of rows to return.
 
         Examples:
             >>> import narwhals as nw
@@ -1819,7 +1818,7 @@ class Expr:
             >>> import pandas as pd
             >>> import polars as pl
             >>> import narwhals as nw
-            >>>
+
             >>> s = [1, 2, 3]
             >>> df_pd = pd.DataFrame({"s": s})
             >>> df_pl = pl.DataFrame({"s": s})
@@ -1913,6 +1912,47 @@ class Expr:
         """
         return self.__class__(lambda plx: self._call(plx).clip(lower_bound, upper_bound))
 
+    def mode(self: Self) -> Self:
+        r"""Compute the most occurring value(s).
+
+        Can return multiple values.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import narwhals as nw
+
+            >>> data = {
+            ...     "a": [1, 1, 2, 3],
+            ...     "b": [1, 1, 2, 2],
+            ... }
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.DataFrame(data)
+
+            We define a library agnostic function:
+
+            >>> @nw.narwhalify
+            ... def func(df):
+            ...     return df.select(nw.col("a").mode()).sort("a")
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(df_pd)
+               a
+            0  1
+
+            >>> func(df_pl)
+            shape: (1, 1)
+            ┌─────┐
+            │ a   │
+            │ --- │
+            │ i64 │
+            ╞═════╡
+            │ 1   │
+            └─────┘
+        """
+        return self.__class__(lambda plx: self._call(plx).mode())
+
     @property
     def str(self: Self) -> ExprStringNamespace:
         return ExprStringNamespace(self)
diff --git a/narwhals/series.py b/narwhals/series.py
index 0b1645ea..9d21058f 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -85,6 +85,58 @@ class Series:
         ca = pa.chunked_array([self.to_arrow()])
         return ca.__arrow_c_stream__(requested_schema=requested_schema)
 
+    def scatter(self, indices: int | Sequence[int], values: Any) -> Self:
+        """
+        Set value(s) at given position(s).
+
+        Arguments:
+           indices: Position(s) to set items at.
+           values: Values to set.
+
+        Warning:
+            For some libraries (pandas, Polars), this method operates in-place,
+            whereas for others (PyArrow) it doesn't!
+            We recommend being careful with it, and not relying on the
+            in-placeness. For example, a valid use case is when updating
+            a column in an eager dataframe, see the example below.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import narwhals as nw
+            >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.DataFrame(data)
+
+            We define a library agnostic function:
+
+            >>> @nw.narwhalify
+            ... def func(df):
+            ...     return df.with_columns(df["a"].scatter([0, 1], [999, 888]))
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(df_pd)
+                 a  b
+            0  999  4
+            1  888  5
+            2    3  6
+            >>> func(df_pl)
+            shape: (3, 2)
+            ┌─────┬─────┐
+            │ a   ┆ b   │
+            │ --- ┆ --- │
+            │ i64 ┆ i64 │
+            ╞═════╪═════╡
+            │ 999 ┆ 4   │
+            │ 888 ┆ 5   │
+            │ 3   ┆ 6   │
+            └─────┴─────┘
+        """
+        return self._from_compliant_series(
+            self._compliant_series.scatter(indices, self._extract_native(values))
+        )
+
     @property
     def shape(self) -> tuple[int]:
         """
@@ -1024,21 +1076,22 @@ class Series:
         return self._from_compliant_series(self._compliant_series.shift(n))
 
     def sample(
-        self,
+        self: Self,
         n: int | None = None,
-        fraction: float | None = None,
         *,
+        fraction: float | None = None,
         with_replacement: bool = False,
+        seed: int | None = None,
     ) -> Self:
         """
         Sample randomly from this Series.
 
         Arguments:
             n: Number of items to return. Cannot be used with fraction.
-
             fraction: Fraction of items to return. Cannot be used with n.
-
             with_replacement: Allow values to be sampled more than once.
+            seed: Seed for the random number generator. If set to None (default), a random
+                seed is generated for each sample operation.
 
         Notes:
             The `sample` method returns a Series with a specified number of
@@ -1079,7 +1132,7 @@ class Series:
         """
         return self._from_compliant_series(
             self._compliant_series.sample(
-                n=n, fraction=fraction, with_replacement=with_replacement
+                n=n, fraction=fraction, with_replacement=with_replacement, seed=seed
             )
         )
 
@@ -2003,9 +2056,8 @@ class Series:
         r"""
         Get the first `n` rows.
 
-        Arguments
-            n : int
-                Number of rows to return.
+        Arguments:
+            n: Number of rows to return.
 
         Examples:
             >>> import narwhals as nw
@@ -2044,9 +2096,8 @@ class Series:
         r"""
         Get the last `n` rows.
 
-        Arguments
-            n : int
-                Number of rows to return.
+        Arguments:
+            n: Number of rows to return.
 
         Examples:
             >>> import narwhals as nw
@@ -2084,7 +2135,7 @@ class Series:
         r"""
         Round underlying floating point data by `decimals` digits.
 
-        Arguments
+        Arguments:
             decimals: Number of decimals to round by.
 
         Notes:
@@ -2134,7 +2185,7 @@ class Series:
         r"""
         Get dummy/indicator variables.
 
-        Arguments
+        Arguments:
             separator: Separator/delimiter used when generating column names.
             drop_first: Remove the first category from the variable being encoded.
 
@@ -2278,6 +2329,44 @@ class Series:
         """
         return self._compliant_series.to_arrow()
 
+    def mode(self: Self) -> Self:
+        r"""
+        Compute the most occurring value(s).
+
+        Can return multiple values.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import narwhals as nw
+
+            >>> data = [1, 1, 2, 2, 3]
+            >>> s_pd = pd.Series(name="a", data=data)
+            >>> s_pl = pl.Series(name="a", values=data)
+
+            We define a library agnostic function:
+
+            >>> @nw.narwhalify
+            ... def func(s):
+            ...     return s.mode().sort()
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(s_pd)
+            0    1
+            1    2
+            Name: a, dtype: int64
+
+            >>> func(s_pl)  # doctest:+NORMALIZE_WHITESPACE
+            shape: (2,)
+            Series: 'a' [i64]
+            [
+               1
+               2
+            ]
+        """
+        return self._from_compliant_series(self._compliant_series.mode())
+
     @property
     def str(self) -> SeriesStringNamespace:
         return SeriesStringNamespace(self)
diff --git a/narwhals/stable/v1.py b/narwhals/stable/v1.py
index 1af7a26f..fa98fd96 100644
--- a/narwhals/stable/v1.py
+++ b/narwhals/stable/v1.py
@@ -78,22 +78,27 @@ class DataFrame(NwDataFrame[IntoDataFrameT]):
     def __getitem__(self, item: tuple[Sequence[int], Sequence[int]]) -> Self: ...
     @overload
     def __getitem__(self, item: tuple[slice, Sequence[int]]) -> Self: ...
-
     @overload
     def __getitem__(self, item: tuple[Sequence[int], str]) -> Series: ...  # type: ignore[overload-overlap]
     @overload
+    def __getitem__(self, item: tuple[slice, str]) -> Series: ...  # type: ignore[overload-overlap]
+    @overload
     def __getitem__(self, item: tuple[Sequence[int], Sequence[str]]) -> Self: ...
     @overload
     def __getitem__(self, item: tuple[slice, Sequence[str]]) -> Self: ...
-
     @overload
     def __getitem__(self, item: tuple[Sequence[int], int]) -> Series: ...  # type: ignore[overload-overlap]
+    @overload
+    def __getitem__(self, item: tuple[slice, int]) -> Series: ...  # type: ignore[overload-overlap]
 
     @overload
     def __getitem__(self, item: Sequence[int]) -> Self: ...
 
     @overload
-    def __getitem__(self, item: str) -> Series: ...
+    def __getitem__(self, item: str) -> Series: ...  # type: ignore[overload-overlap]
+
+    @overload
+    def __getitem__(self, item: Sequence[str]) -> Self: ...
 
     @overload
     def __getitem__(self, item: slice) -> Self: ...
@@ -1541,7 +1546,7 @@ def is_ordered_categorical(series: Series) -> bool:
 
 def maybe_align_index(lhs: T, rhs: Series | DataFrame[Any] | LazyFrame[Any]) -> T:
     """
-    Align `lhs` to the Index of `rhs, if they're both pandas-like.
+    Align `lhs` to the Index of `rhs`, if they're both pandas-like.
 
     Notes:
         This is only really intended for backwards-compatibility purposes,
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 69a99ea2..7b7d09de 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -391,9 +391,6 @@ def from_native(  # noqa: PLR0915
             level="full",
         )
 
-    # TODO(marco): write all of these in terms of `is_` rather
-    # than `get_` + walrus
-
     # Polars
     elif is_polars_dataframe(native_object):
         if series_only:
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 6c1b5c1b..ec3c722d 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -153,7 +153,7 @@ def validate_laziness(items: Iterable[Any]) -> None:
 
 def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T:
     """
-    Align `lhs` to the Index of `rhs, if they're both pandas-like.
+    Align `lhs` to the Index of `rhs`, if they're both pandas-like.
 
     Notes:
         This is only really intended for backwards-compatibility purposes,
diff --git a/pyproject.toml b/pyproject.toml
index 12482a34..efc02f9b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "narwhals"
-version = "1.6.4"
+version = "1.8.1"
 authors = [
   { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
 ]
@@ -114,6 +114,7 @@ filterwarnings = [
   'ignore:.*You are using pyarrow version',
   'ignore:.*but when imported by',
   'ignore:Distributing .*This may take some time',
+  'ignore:.*The default coalesce behavior'
 ]
 xfail_strict = true
 markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 23ff1757..44d57530 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -10,4 +10,5 @@ pytest-cov
 pytest-env
 hypothesis
 scikit-learn
+typing_extensions
 dask[dataframe]; python_version >= '3.9'
diff --git a/tpch/execute/q10.py b/tpch/execute/q10.py
index e1d56d36..124bf0f7 100644
--- a/tpch/execute/q10.py
+++ b/tpch/execute/q10.py
@@ -17,3 +17,7 @@ print(q10.query(fn(customer), fn(nation), fn(lineitem), fn(orders)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q10.query(fn(customer), fn(nation), fn(lineitem), fn(orders)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q10.query(fn(customer), fn(nation), fn(lineitem), fn(orders)).compute())
diff --git a/tpch/execute/q11.py b/tpch/execute/q11.py
index a6b830f3..8c0a2e64 100644
--- a/tpch/execute/q11.py
+++ b/tpch/execute/q11.py
@@ -16,3 +16,7 @@ print(q11.query(fn(nation), fn(partsupp), fn(supplier)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q11.query(fn(nation), fn(partsupp), fn(supplier)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q11.query(fn(nation), fn(partsupp), fn(supplier)).compute())
diff --git a/tpch/execute/q12.py b/tpch/execute/q12.py
index 0cdc0378..3c3a70c6 100644
--- a/tpch/execute/q12.py
+++ b/tpch/execute/q12.py
@@ -15,3 +15,7 @@ print(q12.query(fn(line_item), fn(orders)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q12.query(fn(line_item), fn(orders)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q12.query(fn(line_item), fn(orders)).compute())
diff --git a/tpch/execute/q13.py b/tpch/execute/q13.py
index b5e6c8bb..2fdda5bd 100644
--- a/tpch/execute/q13.py
+++ b/tpch/execute/q13.py
@@ -15,3 +15,7 @@ print(q13.query(fn(customer), fn(orders)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q13.query(fn(customer), fn(orders)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q13.query(fn(customer), fn(orders)).compute())
diff --git a/tpch/execute/q14.py b/tpch/execute/q14.py
index 1a89dbbb..dfd54056 100644
--- a/tpch/execute/q14.py
+++ b/tpch/execute/q14.py
@@ -15,3 +15,7 @@ print(q14.query(fn(line_item), fn(part)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q14.query(fn(line_item), fn(part)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q14.query(fn(line_item), fn(part)).compute())
diff --git a/tpch/execute/q15.py b/tpch/execute/q15.py
index ac858841..86a03b0a 100644
--- a/tpch/execute/q15.py
+++ b/tpch/execute/q15.py
@@ -15,3 +15,7 @@ print(q15.query(fn(lineitem), fn(supplier)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q15.query(fn(lineitem), fn(supplier)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q15.query(fn(lineitem), fn(supplier)).compute())
diff --git a/tpch/execute/q16.py b/tpch/execute/q16.py
index 7fa6c72b..6a70279d 100644
--- a/tpch/execute/q16.py
+++ b/tpch/execute/q16.py
@@ -16,3 +16,7 @@ print(q16.query(fn(part), fn(partsupp), fn(supplier)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q16.query(fn(part), fn(partsupp), fn(supplier)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q16.query(fn(part), fn(partsupp), fn(supplier)).compute())
diff --git a/tpch/execute/q17.py b/tpch/execute/q17.py
index 8eefb92d..43ef4f8b 100644
--- a/tpch/execute/q17.py
+++ b/tpch/execute/q17.py
@@ -15,3 +15,7 @@ print(q17.query(fn(lineitem), fn(part)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q17.query(fn(lineitem), fn(part)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q17.query(fn(lineitem), fn(part)).compute())
diff --git a/tpch/execute/q18.py b/tpch/execute/q18.py
index fdd50c09..c7e5b795 100644
--- a/tpch/execute/q18.py
+++ b/tpch/execute/q18.py
@@ -16,3 +16,7 @@ print(q18.query(fn(customer), fn(lineitem), fn(orders)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q18.query(fn(customer), fn(lineitem), fn(orders)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q18.query(fn(customer), fn(lineitem), fn(orders)).compute())
diff --git a/tpch/execute/q19.py b/tpch/execute/q19.py
index e1dff3eb..60f91b05 100644
--- a/tpch/execute/q19.py
+++ b/tpch/execute/q19.py
@@ -12,3 +12,6 @@ print(q19.query(fn(lineitem), fn(part)).collect())
 
 fn = IO_FUNCS["pyarrow"]
 print(q19.query(fn(lineitem), fn(part)))
+
+fn = IO_FUNCS["dask"]
+print(q19.query(fn(lineitem), fn(part)).compute())
diff --git a/tpch/execute/q20.py b/tpch/execute/q20.py
index d15f8c85..3984b758 100644
--- a/tpch/execute/q20.py
+++ b/tpch/execute/q20.py
@@ -15,3 +15,6 @@ print(q20.query(fn(part), fn(partsupp), fn(nation), fn(lineitem), fn(supplier)).
 
 fn = IO_FUNCS["pyarrow"]
 print(q20.query(fn(part), fn(partsupp), fn(nation), fn(lineitem), fn(supplier)))
+
+fn = IO_FUNCS["dask"]
+print(q20.query(fn(part), fn(partsupp), fn(nation), fn(lineitem), fn(supplier)).compute())
diff --git a/tpch/execute/q21.py b/tpch/execute/q21.py
index 9940e623..7cf772d8 100644
--- a/tpch/execute/q21.py
+++ b/tpch/execute/q21.py
@@ -14,3 +14,6 @@ print(q21.query(fn(lineitem), fn(nation), fn(orders), fn(supplier)).collect())
 
 fn = IO_FUNCS["pyarrow"]
 print(q21.query(fn(lineitem), fn(nation), fn(orders), fn(supplier)))
+
+fn = IO_FUNCS["dask"]
+print(q21.query(fn(lineitem), fn(nation), fn(orders), fn(supplier)).compute())
diff --git a/tpch/execute/q22.py b/tpch/execute/q22.py
index 3b3fe523..a2bb1e76 100644
--- a/tpch/execute/q22.py
+++ b/tpch/execute/q22.py
@@ -15,3 +15,7 @@ print(q22.query(fn(customer), fn(orders)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q22.query(fn(customer), fn(orders)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q22.query(fn(customer), fn(orders)).compute())
diff --git a/tpch/execute/q3.py b/tpch/execute/q3.py
index f836fae2..d6b9302c 100644
--- a/tpch/execute/q3.py
+++ b/tpch/execute/q3.py
@@ -16,3 +16,7 @@ print(q3.query(fn(customer), fn(lineitem), fn(orders)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q3.query(fn(customer), fn(lineitem), fn(orders)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q3.query(fn(customer), fn(lineitem), fn(orders)).compute())
diff --git a/tpch/execute/q4.py b/tpch/execute/q4.py
index ca60f38e..5645574f 100644
--- a/tpch/execute/q4.py
+++ b/tpch/execute/q4.py
@@ -15,3 +15,7 @@ print(q4.query(fn(line_item), fn(orders)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q4.query(fn(line_item), fn(orders)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q4.query(fn(line_item), fn(orders)).compute())
diff --git a/tpch/execute/q5.py b/tpch/execute/q5.py
index c343fea5..dcc61027 100644
--- a/tpch/execute/q5.py
+++ b/tpch/execute/q5.py
@@ -31,3 +31,11 @@ print(
         fn(region), fn(nation), fn(customer), fn(line_item), fn(orders), fn(supplier)
     )
 )
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(
+    q5.query(
+        fn(region), fn(nation), fn(customer), fn(line_item), fn(orders), fn(supplier)
+    ).compute()
+)
diff --git a/tpch/execute/q6.py b/tpch/execute/q6.py
index eebf3f86..154964ff 100644
--- a/tpch/execute/q6.py
+++ b/tpch/execute/q6.py
@@ -14,3 +14,7 @@ print(q6.query(fn(lineitem)).collect())
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q6.query(fn(lineitem)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(q6.query(fn(lineitem)).compute())
diff --git a/tpch/execute/q7.py b/tpch/execute/q7.py
index c59f82ce..a08d5641 100644
--- a/tpch/execute/q7.py
+++ b/tpch/execute/q7.py
@@ -20,3 +20,9 @@ print(
 tool = "pyarrow"
 fn = IO_FUNCS[tool]
 print(q7.query(fn(nation), fn(customer), fn(lineitem), fn(orders), fn(supplier)))
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(
+    q7.query(fn(nation), fn(customer), fn(lineitem), fn(orders), fn(supplier)).compute()
+)
diff --git a/tpch/execute/q8.py b/tpch/execute/q8.py
index 902a34e7..a76a8051 100644
--- a/tpch/execute/q8.py
+++ b/tpch/execute/q8.py
@@ -51,3 +51,17 @@ print(
         fn(region),
     )
 )
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(
+    q8.query(
+        fn(part),
+        fn(supplier),
+        fn(lineitem),
+        fn(orders),
+        fn(customer),
+        fn(nation),
+        fn(region),
+    ).compute()
+)
diff --git a/tpch/execute/q9.py b/tpch/execute/q9.py
index 44d4154a..14230af6 100644
--- a/tpch/execute/q9.py
+++ b/tpch/execute/q9.py
@@ -27,3 +27,11 @@ fn = IO_FUNCS[tool]
 print(
     q9.query(fn(part), fn(partsupp), fn(nation), fn(lineitem), fn(orders), fn(supplier))
 )
+
+tool = "dask"
+fn = IO_FUNCS[tool]
+print(
+    q9.query(
+        fn(part), fn(partsupp), fn(nation), fn(lineitem), fn(orders), fn(supplier)
+    ).compute()
+)
diff --git a/tpch/generate_data.py b/tpch/generate_data.py
index 4d5695dc..5fd73b1f 100644
--- a/tpch/generate_data.py
+++ b/tpch/generate_data.py
@@ -10,7 +10,7 @@ if not Path("data").exists():
 
 con = duckdb.connect(database=":memory:")
 con.execute("INSTALL tpch; LOAD tpch")
-con.execute("CALL dbgen(sf=1)")
+con.execute("CALL dbgen(sf=.5)")
 tables = [
     "lineitem",
     "customer",
diff --git a/tpch/queries/q20.py b/tpch/queries/q20.py
index d9014f7b..b0dabb29 100644
--- a/tpch/queries/q20.py
+++ b/tpch/queries/q20.py
@@ -28,7 +28,8 @@ def query(
 
     return (
         part_ds.filter(nw.col("p_name").str.starts_with(var4))
-        .select(nw.col("p_partkey").unique())
+        .select("p_partkey")
+        .unique("p_partkey")
         .join(partsupp_ds, left_on="p_partkey", right_on="ps_partkey")
         .join(
             query1,
@@ -36,7 +37,8 @@ def query(
             right_on=["l_suppkey", "l_partkey"],
         )
         .filter(nw.col("ps_availqty") > nw.col("sum_quantity"))
-        .select(nw.col("ps_suppkey").unique())
+        .select("ps_suppkey")
+        .unique("ps_suppkey")
         .join(query3, left_on="ps_suppkey", right_on="s_suppkey")
         .select("s_name", "s_address")
         .sort("s_name")
diff --git a/tpch/queries/q22.py b/tpch/queries/q22.py
index 4738c6fd..2e097322 100644
--- a/tpch/queries/q22.py
+++ b/tpch/queries/q22.py
@@ -14,8 +14,10 @@ def query(customer_ds: FrameT, orders_ds: FrameT) -> FrameT:
         nw.col("c_acctbal").mean().alias("avg_acctbal")
     )
 
-    q3 = orders_ds.select(nw.col("o_custkey").unique()).with_columns(
-        nw.col("o_custkey").alias("c_custkey")
+    q3 = (
+        orders_ds.select("o_custkey")
+        .unique("o_custkey")
+        .with_columns(nw.col("o_custkey").alias("c_custkey"))
     )
 
     return (
diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py
index f6e5303c..1bf1f086 100644
--- a/utils/check_api_reference.py
+++ b/utils/check_api_reference.py
@@ -9,15 +9,30 @@ from narwhals.utils import remove_suffix
 
 ret = 0
 
+NAMESPACES = {"dt", "str", "cat", "name"}
+EXPR_ONLY_METHODS = {"over"}
+SERIES_ONLY_METHODS = {
+    "to_arrow",
+    "to_dummies",
+    "to_pandas",
+    "to_list",
+    "to_numpy",
+    "dtype",
+    "name",
+    "shape",
+    "to_frame",
+    "is_empty",
+    "is_sorted",
+    "value_counts",
+    "zip_with",
+    "item",
+    "scatter",
+}
+
 # TODO(Unassigned): make dtypes reference page as well
 files = {remove_suffix(i, ".py") for i in os.listdir("narwhals")}
 top_level_functions = [
-    i
-    for i in nw.__dir__()
-    if not i[0].isupper()
-    and i[0] != "_"
-    and i not in files
-    and i not in {"annotations", "DataFrame", "LazyFrame", "Series"}
+    i for i in nw.__dir__() if not i[0].isupper() and i[0] != "_" and i not in files
 ]
 with open("docs/api-reference/narwhals.md") as fd:
     content = fd.read()
@@ -89,11 +104,7 @@ documented = [
     for i in content.splitlines()
     if i.startswith("        - ") and not i.startswith("        - _")
 ]
-if (
-    missing := set(top_level_functions)
-    .difference(documented)
-    .difference({"dt", "str", "cat", "name"})
-):
+if missing := set(top_level_functions).difference(documented).difference(NAMESPACES):
     print("Series: not documented")  # noqa: T201
     print(missing)  # noqa: T201
     ret = 1
@@ -112,11 +123,7 @@ documented = [
     for i in content.splitlines()
     if i.startswith("        - ")
 ]
-if (
-    missing := set(top_level_functions)
-    .difference(documented)
-    .difference({"cat", "str", "dt", "name"})
-):
+if missing := set(top_level_functions).difference(documented).difference(NAMESPACES):
     print("Expr: not documented")  # noqa: T201
     print(missing)  # noqa: T201
     ret = 1
@@ -139,32 +146,11 @@ series = [
     if not i[0].isupper() and i[0] != "_"
 ]
 
-if missing := set(expr).difference(series).difference({"over"}):
+if missing := set(expr).difference(series).difference(EXPR_ONLY_METHODS):
     print("In expr but not in series")  # noqa: T201
     print(missing)  # noqa: T201
     ret = 1
-if (
-    extra := set(series)
-    .difference(expr)
-    .difference(
-        {
-            "to_arrow",
-            "to_dummies",
-            "to_pandas",
-            "to_list",
-            "to_numpy",
-            "dtype",
-            "name",
-            "shape",
-            "to_frame",
-            "is_empty",
-            "is_sorted",
-            "value_counts",
-            "zip_with",
-            "item",
-        }
-    )
-):
+if extra := set(series).difference(expr).difference(SERIES_ONLY_METHODS):
     print("in series but not in expr")  # noqa: T201
     print(extra)  # noqa: T201
     ret = 1


Test patch:
diff --git a/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
index c733e348..26e9edf7 100644
--- a/.github/workflows/downstream_tests.yml
+++ b/.github/workflows/downstream_tests.yml
@@ -18,8 +18,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: clone-altair
         run: |
             git clone https://github.com/vega/altair.git --depth=1
@@ -58,8 +62,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: clone-scikit-lego
         run: git clone https://github.com/koaning/scikit-lego.git --depth 1
       - name: install-basics
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 265442e9..39b5c91b 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -18,12 +18,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        if: runner.os != 'Windows'
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
-      - name: Install uv (Windows)
-        if: runner.os == 'Windows'
-        run: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install-reqs
         run: uv pip install --upgrade tox virtualenv setuptools -r requirements-dev.txt ibis-framework[duckdb] --system
       - name: show-deps
@@ -45,8 +45,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Windows)
-        run: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install-reqs
         run: uv pip install --upgrade tox virtualenv setuptools -r requirements-dev.txt --system
       - name: install-modin
@@ -70,8 +74,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install-reqs
         run: uv pip install --upgrade tox virtualenv setuptools -r requirements-dev.txt --system
       - name: install-modin
diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml
index e25bdcb6..b029aba3 100644
--- a/.github/workflows/random_ci_pytest.yml
+++ b/.github/workflows/random_ci_pytest.yml
@@ -16,8 +16,12 @@ jobs:
       - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install uv (Unix)
-        run: curl -LsSf https://astral.sh/uv/install.sh | sh
+      - name: Install uv
+        uses: astral-sh/setup-uv@v2
+        with:
+          enable-cache: "true"
+          cache-suffix: ${{ matrix.python-version }}
+          cache-dependency-glob: "**requirements*.txt"
       - name: install package
         run: uv pip install -e . --system
       - name: generate-random-versions
diff --git a/tests/conftest.py b/tests/conftest.py
index cdf4e0be..85c296da 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,6 +13,7 @@ from narwhals.dependencies import get_modin
 from narwhals.typing import IntoDataFrame
 from narwhals.typing import IntoFrame
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 
 with contextlib.suppress(ImportError):
     import modin.pandas  # noqa: F401
@@ -72,9 +73,14 @@ def polars_lazy_constructor(obj: Any) -> pl.LazyFrame:
     return pl.LazyFrame(obj)
 
 
-def dask_lazy_constructor(obj: Any) -> IntoFrame:  # pragma: no cover
+def dask_lazy_p1_constructor(obj: Any) -> IntoFrame:  # pragma: no cover
     dd = get_dask_dataframe()
-    return dd.from_pandas(pd.DataFrame(obj))  # type: ignore[no-any-return]
+    return dd.from_dict(obj, npartitions=1)  # type: ignore[no-any-return]
+
+
+def dask_lazy_p2_constructor(obj: Any) -> IntoFrame:  # pragma: no cover
+    dd = get_dask_dataframe()
+    return dd.from_dict(obj, npartitions=2)  # type: ignore[no-any-return]
 
 
 def pyarrow_table_constructor(obj: Any) -> IntoDataFrame:
@@ -98,14 +104,14 @@ if get_modin() is not None:  # pragma: no cover
 if get_cudf() is not None:
     eager_constructors.append(cudf_constructor)  # pragma: no cover
 if get_dask_dataframe() is not None:  # pragma: no cover
-    lazy_constructors.append(dask_lazy_constructor)  # type: ignore  # noqa: PGH003
+    lazy_constructors.extend([dask_lazy_p1_constructor, dask_lazy_p2_constructor])  # type: ignore  # noqa: PGH003
 
 
 @pytest.fixture(params=eager_constructors)
-def constructor_eager(request: Any) -> Callable[[Any], IntoDataFrame]:
+def constructor_eager(request: pytest.FixtureRequest) -> Callable[[Any], IntoDataFrame]:
     return request.param  # type: ignore[no-any-return]
 
 
 @pytest.fixture(params=[*eager_constructors, *lazy_constructors])
-def constructor(request: Any) -> Callable[[Any], Any]:
+def constructor(request: pytest.FixtureRequest) -> Constructor:
     return request.param  # type: ignore[no-any-return]
diff --git a/tests/expr_and_series/abs_test.py b/tests/expr_and_series/abs_test.py
index e684528b..286bcca1 100644
--- a/tests/expr_and_series/abs_test.py
+++ b/tests/expr_and_series/abs_test.py
@@ -1,10 +1,11 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_abs(constructor: Any) -> None:
+def test_abs(constructor: Constructor) -> None:
     df = nw.from_native(constructor({"a": [1, 2, 3, -4, 5]}))
     result = df.select(b=nw.col("a").abs())
     expected = {"b": [1, 2, 3, 4, 5]}
diff --git a/tests/expr_and_series/all_horizontal_test.py b/tests/expr_and_series/all_horizontal_test.py
index 256c45de..bc9f8035 100644
--- a/tests/expr_and_series/all_horizontal_test.py
+++ b/tests/expr_and_series/all_horizontal_test.py
@@ -3,12 +3,13 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("expr1", ["a", nw.col("a")])
 @pytest.mark.parametrize("expr2", ["b", nw.col("b")])
-def test_allh(constructor: Any, expr1: Any, expr2: Any) -> None:
+def test_allh(constructor: Constructor, expr1: Any, expr2: Any) -> None:
     data = {
         "a": [False, False, True],
         "b": [False, True, True],
diff --git a/tests/expr_and_series/any_all_test.py b/tests/expr_and_series/any_all_test.py
index 09cc8c9e..834a9120 100644
--- a/tests/expr_and_series/any_all_test.py
+++ b/tests/expr_and_series/any_all_test.py
@@ -1,10 +1,11 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_any_all(constructor: Any) -> None:
+def test_any_all(constructor: Constructor) -> None:
     df = nw.from_native(
         constructor(
             {
diff --git a/tests/expr_and_series/any_horizontal_test.py b/tests/expr_and_series/any_horizontal_test.py
index 1f19aa30..1b6dfd48 100644
--- a/tests/expr_and_series/any_horizontal_test.py
+++ b/tests/expr_and_series/any_horizontal_test.py
@@ -3,12 +3,13 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("expr1", ["a", nw.col("a")])
 @pytest.mark.parametrize("expr2", ["b", nw.col("b")])
-def test_anyh(constructor: Any, expr1: Any, expr2: Any) -> None:
+def test_anyh(constructor: Constructor, expr1: Any, expr2: Any) -> None:
     data = {
         "a": [False, False, True],
         "b": [False, True, True],
diff --git a/tests/expr_and_series/arg_true_test.py b/tests/expr_and_series/arg_true_test.py
index eaa3d1ba..7e1262aa 100644
--- a/tests/expr_and_series/arg_true_test.py
+++ b/tests/expr_and_series/arg_true_test.py
@@ -3,10 +3,11 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_arg_true(constructor: Any, request: Any) -> None:
+def test_arg_true(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor({"a": [1, None, None, 3]}))
diff --git a/tests/expr_and_series/arithmetic_test.py b/tests/expr_and_series/arithmetic_test.py
index 7ff945c8..e431aebb 100644
--- a/tests/expr_and_series/arithmetic_test.py
+++ b/tests/expr_and_series/arithmetic_test.py
@@ -12,6 +12,7 @@ from hypothesis import given
 
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
@@ -32,8 +33,8 @@ def test_arithmetic_expr(
     attr: str,
     rhs: Any,
     expected: list[Any],
-    constructor: Any,
-    request: Any,
+    constructor: Constructor,
+    request: pytest.FixtureRequest,
 ) -> None:
     if attr == "__mod__" and any(
         x in str(constructor) for x in ["pandas_pyarrow", "modin"]
@@ -62,8 +63,8 @@ def test_right_arithmetic_expr(
     attr: str,
     rhs: Any,
     expected: list[Any],
-    constructor: Any,
-    request: Any,
+    constructor: Constructor,
+    request: pytest.FixtureRequest,
 ) -> None:
     if attr == "__rmod__" and any(
         x in str(constructor) for x in ["pandas_pyarrow", "modin"]
@@ -94,7 +95,7 @@ def test_arithmetic_series(
     rhs: Any,
     expected: list[Any],
     constructor_eager: Any,
-    request: Any,
+    request: pytest.FixtureRequest,
 ) -> None:
     if attr == "__mod__" and any(
         x in str(constructor_eager) for x in ["pandas_pyarrow", "modin"]
@@ -124,7 +125,7 @@ def test_right_arithmetic_series(
     rhs: Any,
     expected: list[Any],
     constructor_eager: Any,
-    request: Any,
+    request: pytest.FixtureRequest,
 ) -> None:
     if attr == "__rmod__" and any(
         x in str(constructor_eager) for x in ["pandas_pyarrow", "modin"]
@@ -137,7 +138,9 @@ def test_right_arithmetic_series(
     compare_dicts(result, {"a": expected})
 
 
-def test_truediv_same_dims(constructor_eager: Any, request: Any) -> None:
+def test_truediv_same_dims(
+    constructor_eager: Any, request: pytest.FixtureRequest
+) -> None:
     if "polars" in str(constructor_eager):
         # https://github.com/pola-rs/polars/issues/17760
         request.applymarker(pytest.mark.xfail)
diff --git a/tests/expr_and_series/binary_test.py b/tests/expr_and_series/binary_test.py
index 2d55af22..1ce76d9d 100644
--- a/tests/expr_and_series/binary_test.py
+++ b/tests/expr_and_series/binary_test.py
@@ -1,10 +1,9 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_expr_binary(constructor: Any) -> None:
+def test_expr_binary(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df_raw = constructor(data)
     result = nw.from_native(df_raw).with_columns(
diff --git a/tests/expr_and_series/cast_test.py b/tests/expr_and_series/cast_test.py
index 0b496d7a..00f24214 100644
--- a/tests/expr_and_series/cast_test.py
+++ b/tests/expr_and_series/cast_test.py
@@ -1,11 +1,10 @@
-from typing import Any
-
 import pandas as pd
 import pyarrow as pa
 import pytest
 
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 
 data = {
     "a": [1],
@@ -46,7 +45,7 @@ schema = {
 
 
 @pytest.mark.filterwarnings("ignore:casting period[M] values to int64:FutureWarning")
-def test_cast(constructor: Any, request: Any) -> None:
+def test_cast(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "pyarrow_table_constructor" in str(constructor) and parse_version(
         pa.__version__
     ) <= (15,):  # pragma: no cover
@@ -96,7 +95,7 @@ def test_cast(constructor: Any, request: Any) -> None:
     assert dict(result.collect_schema()) == expected
 
 
-def test_cast_series(constructor: Any, request: Any) -> None:
+def test_cast_series(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "pyarrow_table_constructor" in str(constructor) and parse_version(
         pa.__version__
     ) <= (15,):  # pragma: no cover
@@ -162,7 +161,9 @@ def test_cast_string() -> None:
     assert str(result.dtype) in ("string", "object", "dtype('O')")
 
 
-def test_cast_raises_for_unknown_dtype(constructor: Any, request: Any) -> None:
+def test_cast_raises_for_unknown_dtype(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
     if "pyarrow_table_constructor" in str(constructor) and parse_version(
         pa.__version__
     ) <= (15,):  # pragma: no cover
diff --git a/tests/expr_and_series/cat/get_categories_test.py b/tests/expr_and_series/cat/get_categories_test.py
index 6432826c..122f3c83 100644
--- a/tests/expr_and_series/cat/get_categories_test.py
+++ b/tests/expr_and_series/cat/get_categories_test.py
@@ -12,7 +12,7 @@ from tests.utils import compare_dicts
 data = {"a": ["one", "two", "two"]}
 
 
-def test_get_categories(request: Any, constructor_eager: Any) -> None:
+def test_get_categories(request: pytest.FixtureRequest, constructor_eager: Any) -> None:
     if "pyarrow_table" in str(constructor_eager) and parse_version(
         pa.__version__
     ) < parse_version("15.0.0"):
diff --git a/tests/expr_and_series/clip_test.py b/tests/expr_and_series/clip_test.py
index 909b153b..d3f90633 100644
--- a/tests/expr_and_series/clip_test.py
+++ b/tests/expr_and_series/clip_test.py
@@ -1,10 +1,11 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_clip(constructor: Any) -> None:
+def test_clip(constructor: Constructor) -> None:
     df = nw.from_native(constructor({"a": [1, 2, 3, -4, 5]}))
     result = df.select(
         lower_only=nw.col("a").clip(lower_bound=3),
diff --git a/tests/expr_and_series/count_test.py b/tests/expr_and_series/count_test.py
index 208df3bc..580bd202 100644
--- a/tests/expr_and_series/count_test.py
+++ b/tests/expr_and_series/count_test.py
@@ -1,10 +1,11 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_count(constructor: Any) -> None:
+def test_count(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, None, 6], "z": [7.0, None, None]}
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a", "b", "z").count())
diff --git a/tests/expr_and_series/cum_sum_test.py b/tests/expr_and_series/cum_sum_test.py
index e169b28f..94897a85 100644
--- a/tests/expr_and_series/cum_sum_test.py
+++ b/tests/expr_and_series/cum_sum_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -10,7 +11,7 @@ data = {
 }
 
 
-def test_cum_sum_simple(constructor: Any) -> None:
+def test_cum_sum_simple(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a", "b", "c").cum_sum())
     expected = {
diff --git a/tests/expr_and_series/diff_test.py b/tests/expr_and_series/diff_test.py
index f38b96e0..33445f76 100644
--- a/tests/expr_and_series/diff_test.py
+++ b/tests/expr_and_series/diff_test.py
@@ -5,6 +5,7 @@ import pytest
 
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -14,7 +15,7 @@ data = {
 }
 
 
-def test_diff(constructor: Any, request: Any) -> None:
+def test_diff(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "pyarrow_table_constructor" in str(constructor) and parse_version(
         pa.__version__
     ) < (13,):
@@ -31,7 +32,7 @@ def test_diff(constructor: Any, request: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_diff_series(constructor_eager: Any, request: Any) -> None:
+def test_diff_series(constructor_eager: Any, request: pytest.FixtureRequest) -> None:
     if "pyarrow_table_constructor" in str(constructor_eager) and parse_version(
         pa.__version__
     ) < (13,):
diff --git a/tests/expr_and_series/double_selected_test.py b/tests/expr_and_series/double_selected_test.py
index 7b8fd670..88826fb4 100644
--- a/tests/expr_and_series/double_selected_test.py
+++ b/tests/expr_and_series/double_selected_test.py
@@ -1,10 +1,9 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_double_selected(constructor: Any) -> None:
+def test_double_selected(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
 
diff --git a/tests/expr_and_series/double_test.py b/tests/expr_and_series/double_test.py
index 3a6b622b..8f19e020 100644
--- a/tests/expr_and_series/double_test.py
+++ b/tests/expr_and_series/double_test.py
@@ -1,10 +1,9 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_double(constructor: Any) -> None:
+def test_double(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.with_columns(nw.all() * 2)
@@ -12,7 +11,7 @@ def test_double(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_double_alias(constructor: Any) -> None:
+def test_double_alias(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.with_columns(nw.col("a").alias("o"), nw.all() * 2)
diff --git a/tests/expr_and_series/drop_nulls_test.py b/tests/expr_and_series/drop_nulls_test.py
index f4c8e2d7..bc06eec3 100644
--- a/tests/expr_and_series/drop_nulls_test.py
+++ b/tests/expr_and_series/drop_nulls_test.py
@@ -5,10 +5,11 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_drop_nulls(constructor: Any, request: Any) -> None:
+def test_drop_nulls(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     data = {
diff --git a/tests/expr_and_series/dt/datetime_attributes_test.py b/tests/expr_and_series/dt/datetime_attributes_test.py
index 22e20590..5b9519f5 100644
--- a/tests/expr_and_series/dt/datetime_attributes_test.py
+++ b/tests/expr_and_series/dt/datetime_attributes_test.py
@@ -7,6 +7,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -34,7 +35,10 @@ data = {
     ],
 )
 def test_datetime_attributes(
-    request: Any, constructor: Any, attribute: str, expected: list[int]
+    request: pytest.FixtureRequest,
+    constructor: Constructor,
+    attribute: str,
+    expected: list[int],
 ) -> None:
     if (
         attribute == "date"
@@ -67,7 +71,10 @@ def test_datetime_attributes(
     ],
 )
 def test_datetime_attributes_series(
-    request: Any, constructor_eager: Any, attribute: str, expected: list[int]
+    request: pytest.FixtureRequest,
+    constructor_eager: Any,
+    attribute: str,
+    expected: list[int],
 ) -> None:
     if (
         attribute == "date"
@@ -83,7 +90,9 @@ def test_datetime_attributes_series(
     compare_dicts(result, {"a": expected})
 
 
-def test_datetime_chained_attributes(request: Any, constructor_eager: Any) -> None:
+def test_datetime_chained_attributes(
+    request: pytest.FixtureRequest, constructor_eager: Any
+) -> None:
     if "pandas" in str(constructor_eager) and "pyarrow" not in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
     if "cudf" in str(constructor_eager):
diff --git a/tests/expr_and_series/dt/datetime_duration_test.py b/tests/expr_and_series/dt/datetime_duration_test.py
index 50d254ba..da5ff325 100644
--- a/tests/expr_and_series/dt/datetime_duration_test.py
+++ b/tests/expr_and_series/dt/datetime_duration_test.py
@@ -11,6 +11,7 @@ import pytest
 
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -37,8 +38,8 @@ data = {
     ],
 )
 def test_duration_attributes(
-    request: Any,
-    constructor: Any,
+    request: pytest.FixtureRequest,
+    constructor: Constructor,
     attribute: str,
     expected_a: list[int],
     expected_b: list[int],
@@ -46,6 +47,8 @@ def test_duration_attributes(
 ) -> None:
     if parse_version(pd.__version__) < (2, 2) and "pandas_pyarrow" in str(constructor):
         request.applymarker(pytest.mark.xfail)
+    if "cudf" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
 
     df = nw.from_native(constructor(data))
 
@@ -70,7 +73,7 @@ def test_duration_attributes(
     ],
 )
 def test_duration_attributes_series(
-    request: Any,
+    request: pytest.FixtureRequest,
     constructor_eager: Any,
     attribute: str,
     expected_a: list[int],
@@ -81,6 +84,8 @@ def test_duration_attributes_series(
         constructor_eager
     ):
         request.applymarker(pytest.mark.xfail)
+    if "cudf" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
 
     df = nw.from_native(constructor_eager(data), eager_only=True)
 
diff --git a/tests/expr_and_series/dt/to_string_test.py b/tests/expr_and_series/dt/to_string_test.py
index 7cbbf72f..6017c33d 100644
--- a/tests/expr_and_series/dt/to_string_test.py
+++ b/tests/expr_and_series/dt/to_string_test.py
@@ -6,6 +6,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 from tests.utils import is_windows
 
@@ -57,7 +58,7 @@ def test_dt_to_string_series(constructor_eager: Any, fmt: str) -> None:
     ],
 )
 @pytest.mark.skipif(is_windows(), reason="pyarrow breaking on windows")
-def test_dt_to_string_expr(constructor: Any, fmt: str) -> None:
+def test_dt_to_string_expr(constructor: Constructor, fmt: str) -> None:
     input_frame = nw.from_native(constructor(data))
 
     expected_col = [datetime.strftime(d, fmt) for d in data["a"]]
@@ -130,10 +131,8 @@ def test_dt_to_string_iso_local_datetime_series(
 )
 @pytest.mark.skipif(is_windows(), reason="pyarrow breaking on windows")
 def test_dt_to_string_iso_local_datetime_expr(
-    request: Any, constructor: Any, data: datetime, expected: str
+    constructor: Constructor, data: datetime, expected: str
 ) -> None:
-    if "modin" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
     df = constructor({"a": [data]})
 
     result = nw.from_native(df).with_columns(
@@ -166,11 +165,8 @@ def test_dt_to_string_iso_local_date_series(
 )
 @pytest.mark.skipif(is_windows(), reason="pyarrow breaking on windows")
 def test_dt_to_string_iso_local_date_expr(
-    request: Any, constructor: Any, data: datetime, expected: str
+    constructor: Constructor, data: datetime, expected: str
 ) -> None:
-    if "modin" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     df = constructor({"a": [data]})
     result = nw.from_native(df).with_columns(
         nw.col("a").dt.to_string("%Y-%m-%d").alias("b")
diff --git a/tests/expr_and_series/fill_null_test.py b/tests/expr_and_series/fill_null_test.py
index 04d6d076..6efde5ac 100644
--- a/tests/expr_and_series/fill_null_test.py
+++ b/tests/expr_and_series/fill_null_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -10,7 +11,7 @@ data = {
 }
 
 
-def test_fill_null(constructor: Any) -> None:
+def test_fill_null(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
 
     result = df.with_columns(nw.col("a", "b", "c").fill_null(99))
diff --git a/tests/expr_and_series/filter_test.py b/tests/expr_and_series/filter_test.py
index b55a0368..80267d1d 100644
--- a/tests/expr_and_series/filter_test.py
+++ b/tests/expr_and_series/filter_test.py
@@ -3,6 +3,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -13,7 +14,7 @@ data = {
 }
 
 
-def test_filter(constructor: Any, request: Any) -> None:
+def test_filter(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data))
diff --git a/tests/expr_and_series/gather_every_test.py b/tests/expr_and_series/gather_every_test.py
index b00014f2..e01294ef 100644
--- a/tests/expr_and_series/gather_every_test.py
+++ b/tests/expr_and_series/gather_every_test.py
@@ -3,6 +3,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": list(range(10))}
@@ -10,7 +11,9 @@ data = {"a": list(range(10))}
 
 @pytest.mark.parametrize("n", [1, 2, 3])
 @pytest.mark.parametrize("offset", [1, 2, 3])
-def test_gather_every_expr(constructor: Any, n: int, offset: int, request: Any) -> None:
+def test_gather_every_expr(
+    constructor: Constructor, n: int, offset: int, request: pytest.FixtureRequest
+) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data))
diff --git a/tests/expr_and_series/head_test.py b/tests/expr_and_series/head_test.py
index ef2ed1bf..2a632692 100644
--- a/tests/expr_and_series/head_test.py
+++ b/tests/expr_and_series/head_test.py
@@ -5,11 +5,12 @@ from typing import Any
 import pytest
 
 import narwhals as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("n", [2, -1])
-def test_head(constructor: Any, n: int, request: Any) -> None:
+def test_head(constructor: Constructor, n: int, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     if "polars" in str(constructor) and n < 0:
diff --git a/tests/expr_and_series/is_between_test.py b/tests/expr_and_series/is_between_test.py
index 10c61e9e..0a9e578e 100644
--- a/tests/expr_and_series/is_between_test.py
+++ b/tests/expr_and_series/is_between_test.py
@@ -5,6 +5,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -21,7 +22,7 @@ data = {
         ("none", [False, True, True, False]),
     ],
 )
-def test_is_between(constructor: Any, closed: str, expected: list[bool]) -> None:
+def test_is_between(constructor: Constructor, closed: str, expected: list[bool]) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a").is_between(1, 5, closed=closed))
     expected_dict = {"a": expected}
diff --git a/tests/expr_and_series/is_duplicated_test.py b/tests/expr_and_series/is_duplicated_test.py
index 71d16574..7859aed0 100644
--- a/tests/expr_and_series/is_duplicated_test.py
+++ b/tests/expr_and_series/is_duplicated_test.py
@@ -1,12 +1,13 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 1, 2], "b": [1, 2, 3], "index": [0, 1, 2]}
 
 
-def test_is_duplicated_expr(constructor: Any) -> None:
+def test_is_duplicated_expr(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a", "b").is_duplicated(), "index").sort("index")
     expected = {"a": [True, True, False], "b": [False, False, False], "index": [0, 1, 2]}
diff --git a/tests/expr_and_series/is_first_distinct_test.py b/tests/expr_and_series/is_first_distinct_test.py
index 8521661d..93ffc5d3 100644
--- a/tests/expr_and_series/is_first_distinct_test.py
+++ b/tests/expr_and_series/is_first_distinct_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -9,7 +10,7 @@ data = {
 }
 
 
-def test_is_first_distinct_expr(constructor: Any) -> None:
+def test_is_first_distinct_expr(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.all().is_first_distinct())
     expected = {
diff --git a/tests/expr_and_series/is_in_test.py b/tests/expr_and_series/is_in_test.py
index 40c7b271..085b1efb 100644
--- a/tests/expr_and_series/is_in_test.py
+++ b/tests/expr_and_series/is_in_test.py
@@ -3,12 +3,13 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 4, 2, 5]}
 
 
-def test_expr_is_in(constructor: Any) -> None:
+def test_expr_is_in(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a").is_in([4, 5]))
     expected = {"a": [False, True, False, True]}
@@ -24,7 +25,7 @@ def test_ser_is_in(constructor_eager: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_is_in_other(constructor: Any) -> None:
+def test_is_in_other(constructor: Constructor) -> None:
     df_raw = constructor(data)
     with pytest.raises(
         NotImplementedError,
diff --git a/tests/expr_and_series/is_last_distinct_test.py b/tests/expr_and_series/is_last_distinct_test.py
index 2e4709ef..00db7f73 100644
--- a/tests/expr_and_series/is_last_distinct_test.py
+++ b/tests/expr_and_series/is_last_distinct_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -9,7 +10,7 @@ data = {
 }
 
 
-def test_is_last_distinct_expr(constructor: Any) -> None:
+def test_is_last_distinct_expr(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.all().is_last_distinct())
     expected = {
diff --git a/tests/expr_and_series/is_null_test.py b/tests/expr_and_series/is_null_test.py
index 07465fd9..85ba55dc 100644
--- a/tests/expr_and_series/is_null_test.py
+++ b/tests/expr_and_series/is_null_test.py
@@ -1,10 +1,11 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_null(constructor: Any) -> None:
+def test_null(constructor: Constructor) -> None:
     data_na = {"a": [None, 3, 2], "z": [7.0, None, None]}
     expected = {"a": [True, False, False], "z": [True, False, False]}
     df = nw.from_native(constructor(data_na))
diff --git a/tests/expr_and_series/is_unique_test.py b/tests/expr_and_series/is_unique_test.py
index d203c163..b10f7a68 100644
--- a/tests/expr_and_series/is_unique_test.py
+++ b/tests/expr_and_series/is_unique_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -10,7 +11,7 @@ data = {
 }
 
 
-def test_is_unique_expr(constructor: Any) -> None:
+def test_is_unique_expr(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a", "b").is_unique(), "index").sort("index")
     expected = {
diff --git a/tests/expr_and_series/len_test.py b/tests/expr_and_series/len_test.py
index 8a52dd32..b1e1674b 100644
--- a/tests/expr_and_series/len_test.py
+++ b/tests/expr_and_series/len_test.py
@@ -3,10 +3,11 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_len_no_filter(constructor: Any) -> None:
+def test_len_no_filter(constructor: Constructor) -> None:
     data = {"a": list("xyz"), "b": [1, 2, 1]}
     expected = {"l": [3], "l2": [6]}
     df = nw.from_native(constructor(data)).select(
@@ -17,7 +18,7 @@ def test_len_no_filter(constructor: Any) -> None:
     compare_dicts(df, expected)
 
 
-def test_len_chaining(constructor: Any, request: Any) -> None:
+def test_len_chaining(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     data = {"a": list("xyz"), "b": [1, 2, 1]}
     expected = {"a1": [2], "a2": [1]}
     if "dask" in str(constructor):
@@ -30,7 +31,7 @@ def test_len_chaining(constructor: Any, request: Any) -> None:
     compare_dicts(df, expected)
 
 
-def test_namespace_len(constructor: Any) -> None:
+def test_namespace_len(constructor: Constructor) -> None:
     df = nw.from_native(constructor({"a": [1, 2, 3], "b": [4, 5, 6]})).select(
         nw.len(), a=nw.len()
     )
diff --git a/tests/expr_and_series/max_test.py b/tests/expr_and_series/max_test.py
index 83f24dcf..1ea32531 100644
--- a/tests/expr_and_series/max_test.py
+++ b/tests/expr_and_series/max_test.py
@@ -5,13 +5,14 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
 
 
 @pytest.mark.parametrize("expr", [nw.col("a", "b", "z").max(), nw.max("a", "b", "z")])
-def test_expr_max_expr(constructor: Any, expr: nw.Expr) -> None:
+def test_expr_max_expr(constructor: Constructor, expr: nw.Expr) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(expr)
     expected = {"a": [3], "b": [6], "z": [9.0]}
diff --git a/tests/expr_and_series/mean_horizontal_test.py b/tests/expr_and_series/mean_horizontal_test.py
index d42d5e32..f4ad35b9 100644
--- a/tests/expr_and_series/mean_horizontal_test.py
+++ b/tests/expr_and_series/mean_horizontal_test.py
@@ -3,11 +3,12 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("col_expr", [nw.col("a"), "a"])
-def test_meanh(constructor: Any, col_expr: Any) -> None:
+def test_meanh(constructor: Constructor, col_expr: Any) -> None:
     data = {"a": [1, 3, None, None], "b": [4, None, 6, None]}
     df = nw.from_native(constructor(data))
     result = df.select(horizontal_mean=nw.mean_horizontal(col_expr, nw.col("b")))
diff --git a/tests/expr_and_series/mean_test.py b/tests/expr_and_series/mean_test.py
index f648c9b3..50e6fd86 100644
--- a/tests/expr_and_series/mean_test.py
+++ b/tests/expr_and_series/mean_test.py
@@ -5,13 +5,14 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 3, 2], "b": [4, 4, 7], "z": [7.0, 8, 9]}
 
 
 @pytest.mark.parametrize("expr", [nw.col("a", "b", "z").mean(), nw.mean("a", "b", "z")])
-def test_expr_mean_expr(constructor: Any, expr: nw.Expr) -> None:
+def test_expr_mean_expr(constructor: Constructor, expr: nw.Expr) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(expr)
     expected = {"a": [2.0], "b": [5.0], "z": [8.0]}
diff --git a/tests/expr_and_series/min_test.py b/tests/expr_and_series/min_test.py
index 460e5646..f6e98e41 100644
--- a/tests/expr_and_series/min_test.py
+++ b/tests/expr_and_series/min_test.py
@@ -5,13 +5,14 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
 
 
 @pytest.mark.parametrize("expr", [nw.col("a", "b", "z").min(), nw.min("a", "b", "z")])
-def test_expr_min_expr(constructor: Any, expr: nw.Expr) -> None:
+def test_expr_min_expr(constructor: Constructor, expr: nw.Expr) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(expr)
     expected = {"a": [1], "b": [4], "z": [7.0]}
diff --git a/tests/expr_and_series/mode_test.py b/tests/expr_and_series/mode_test.py
new file mode 100644
index 00000000..8e39405a
--- /dev/null
+++ b/tests/expr_and_series/mode_test.py
@@ -0,0 +1,46 @@
+from typing import Any
+
+import polars as pl
+import pytest
+
+import narwhals.stable.v1 as nw
+from narwhals.utils import parse_version
+from tests.utils import Constructor
+from tests.utils import compare_dicts
+
+data = {
+    "a": [1, 1, 2, 2, 3],
+    "b": [1, 2, 3, 3, 4],
+}
+
+
+def test_mode_single_expr(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
+    if "dask" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    df = nw.from_native(constructor(data))
+    result = df.select(nw.col("a").mode()).sort("a")
+    expected = {"a": [1, 2]}
+    compare_dicts(result, expected)
+
+
+def test_mode_multi_expr(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
+    if "dask" in str(constructor) or (
+        "polars" in str(constructor) and parse_version(pl.__version__) >= (1, 7, 0)
+    ):
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(constructor(data))
+    result = df.select(nw.col("a", "b").mode()).sort("a", "b")
+    expected = {"a": [1, 2], "b": [3, 3]}
+    compare_dicts(result, expected)
+
+
+def test_mode_series(constructor_eager: Any) -> None:
+    series = nw.from_native(constructor_eager(data), eager_only=True)["a"]
+    result = series.mode().sort()
+    expected = {"a": [1, 2]}
+    compare_dicts({"a": result}, expected)
diff --git a/tests/expr_and_series/n_unique_test.py b/tests/expr_and_series/n_unique_test.py
index f11be2b1..3790bb1f 100644
--- a/tests/expr_and_series/n_unique_test.py
+++ b/tests/expr_and_series/n_unique_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -9,7 +10,7 @@ data = {
 }
 
 
-def test_n_unique(constructor: Any) -> None:
+def test_n_unique(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.all().n_unique())
     expected = {"a": [3], "b": [4]}
diff --git a/tests/expr_and_series/name/keep_test.py b/tests/expr_and_series/name/keep_test.py
index 0b43abe4..be112d71 100644
--- a/tests/expr_and_series/name/keep_test.py
+++ b/tests/expr_and_series/name/keep_test.py
@@ -1,32 +1,32 @@
 from __future__ import annotations
 
 from contextlib import nullcontext as does_not_raise
-from typing import Any
 
 import polars as pl
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 
 
-def test_keep(constructor: Any) -> None:
+def test_keep(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.keep())
     expected = {k: [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_keep_after_alias(constructor: Any) -> None:
+def test_keep_after_alias(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.keep())
     expected = {"foo": data["foo"]}
     compare_dicts(result, expected)
 
 
-def test_keep_raise_anonymous(constructor: Any) -> None:
+def test_keep_raise_anonymous(constructor: Constructor) -> None:
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
 
diff --git a/tests/expr_and_series/name/map_test.py b/tests/expr_and_series/name/map_test.py
index ff039e30..5fad9f93 100644
--- a/tests/expr_and_series/name/map_test.py
+++ b/tests/expr_and_series/name/map_test.py
@@ -1,12 +1,12 @@
 from __future__ import annotations
 
 from contextlib import nullcontext as does_not_raise
-from typing import Any
 
 import polars as pl
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
@@ -16,21 +16,21 @@ def map_func(s: str | None) -> str:
     return str(s)[::-1].lower()
 
 
-def test_map(constructor: Any) -> None:
+def test_map(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.map(function=map_func))
     expected = {map_func(k): [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_map_after_alias(constructor: Any) -> None:
+def test_map_after_alias(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.map(function=map_func))
     expected = {map_func("foo"): data["foo"]}
     compare_dicts(result, expected)
 
 
-def test_map_raise_anonymous(constructor: Any) -> None:
+def test_map_raise_anonymous(constructor: Constructor) -> None:
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
 
diff --git a/tests/expr_and_series/name/prefix_test.py b/tests/expr_and_series/name/prefix_test.py
index f538d413..95d72914 100644
--- a/tests/expr_and_series/name/prefix_test.py
+++ b/tests/expr_and_series/name/prefix_test.py
@@ -1,33 +1,33 @@
 from __future__ import annotations
 
 from contextlib import nullcontext as does_not_raise
-from typing import Any
 
 import polars as pl
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 prefix = "with_prefix_"
 
 
-def test_prefix(constructor: Any) -> None:
+def test_prefix(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.prefix(prefix))
     expected = {prefix + str(k): [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_suffix_after_alias(constructor: Any) -> None:
+def test_suffix_after_alias(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.prefix(prefix))
     expected = {prefix + "foo": data["foo"]}
     compare_dicts(result, expected)
 
 
-def test_prefix_raise_anonymous(constructor: Any) -> None:
+def test_prefix_raise_anonymous(constructor: Constructor) -> None:
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
 
diff --git a/tests/expr_and_series/name/suffix_test.py b/tests/expr_and_series/name/suffix_test.py
index 0e952449..1802f26f 100644
--- a/tests/expr_and_series/name/suffix_test.py
+++ b/tests/expr_and_series/name/suffix_test.py
@@ -1,33 +1,33 @@
 from __future__ import annotations
 
 from contextlib import nullcontext as does_not_raise
-from typing import Any
 
 import polars as pl
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 suffix = "_with_suffix"
 
 
-def test_suffix(constructor: Any) -> None:
+def test_suffix(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.suffix(suffix))
     expected = {str(k) + suffix: [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_suffix_after_alias(constructor: Any) -> None:
+def test_suffix_after_alias(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.suffix(suffix))
     expected = {"foo" + suffix: data["foo"]}
     compare_dicts(result, expected)
 
 
-def test_suffix_raise_anonymous(constructor: Any) -> None:
+def test_suffix_raise_anonymous(constructor: Constructor) -> None:
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
 
diff --git a/tests/expr_and_series/name/to_lowercase_test.py b/tests/expr_and_series/name/to_lowercase_test.py
index a9e8bfcf..fedac9cd 100644
--- a/tests/expr_and_series/name/to_lowercase_test.py
+++ b/tests/expr_and_series/name/to_lowercase_test.py
@@ -1,32 +1,32 @@
 from __future__ import annotations
 
 from contextlib import nullcontext as does_not_raise
-from typing import Any
 
 import polars as pl
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 
 
-def test_to_lowercase(constructor: Any) -> None:
+def test_to_lowercase(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.to_lowercase())
     expected = {k.lower(): [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_to_lowercase_after_alias(constructor: Any) -> None:
+def test_to_lowercase_after_alias(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("BAR")).alias("ALIAS_FOR_BAR").name.to_lowercase())
     expected = {"bar": data["BAR"]}
     compare_dicts(result, expected)
 
 
-def test_to_lowercase_raise_anonymous(constructor: Any) -> None:
+def test_to_lowercase_raise_anonymous(constructor: Constructor) -> None:
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
 
diff --git a/tests/expr_and_series/name/to_uppercase_test.py b/tests/expr_and_series/name/to_uppercase_test.py
index 035dfeff..29b70bd9 100644
--- a/tests/expr_and_series/name/to_uppercase_test.py
+++ b/tests/expr_and_series/name/to_uppercase_test.py
@@ -1,32 +1,32 @@
 from __future__ import annotations
 
 from contextlib import nullcontext as does_not_raise
-from typing import Any
 
 import polars as pl
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"foo": [1, 2, 3], "BAR": [4, 5, 6]}
 
 
-def test_to_uppercase(constructor: Any) -> None:
+def test_to_uppercase(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo", "BAR") * 2).name.to_uppercase())
     expected = {k.upper(): [e * 2 for e in v] for k, v in data.items()}
     compare_dicts(result, expected)
 
 
-def test_to_uppercase_after_alias(constructor: Any) -> None:
+def test_to_uppercase_after_alias(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select((nw.col("foo")).alias("alias_for_foo").name.to_uppercase())
     expected = {"FOO": data["foo"]}
     compare_dicts(result, expected)
 
 
-def test_to_uppercase_raise_anonymous(constructor: Any) -> None:
+def test_to_uppercase_raise_anonymous(constructor: Constructor) -> None:
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
 
diff --git a/tests/expr_and_series/null_count_test.py b/tests/expr_and_series/null_count_test.py
index a6cb58f7..6be15ab3 100644
--- a/tests/expr_and_series/null_count_test.py
+++ b/tests/expr_and_series/null_count_test.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -9,7 +10,7 @@ data = {
 }
 
 
-def test_null_count_expr(constructor: Any) -> None:
+def test_null_count_expr(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.all().null_count())
     expected = {
diff --git a/tests/expr_and_series/operators_test.py b/tests/expr_and_series/operators_test.py
index 113824a9..e3f39465 100644
--- a/tests/expr_and_series/operators_test.py
+++ b/tests/expr_and_series/operators_test.py
@@ -5,6 +5,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
@@ -20,7 +21,7 @@ from tests.utils import compare_dicts
     ],
 )
 def test_comparand_operators_scalar_expr(
-    constructor: Any, operator: str, expected: list[bool]
+    constructor: Constructor, operator: str, expected: list[bool]
 ) -> None:
     data = {"a": [0, 1, 2]}
     df = nw.from_native(constructor(data))
@@ -40,7 +41,7 @@ def test_comparand_operators_scalar_expr(
     ],
 )
 def test_comparand_operators_expr(
-    constructor: Any, operator: str, expected: list[bool]
+    constructor: Constructor, operator: str, expected: list[bool]
 ) -> None:
     data = {"a": [0, 1, 1], "b": [0, 0, 2]}
     df = nw.from_native(constructor(data))
@@ -56,7 +57,7 @@ def test_comparand_operators_expr(
     ],
 )
 def test_logic_operators_expr(
-    constructor: Any, operator: str, expected: list[bool]
+    constructor: Constructor, operator: str, expected: list[bool]
 ) -> None:
     data = {"a": [True, True, False, False], "b": [True, False, True, False]}
     df = nw.from_native(constructor(data))
diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py
index fb01a3cf..2abc9a69 100644
--- a/tests/expr_and_series/over_test.py
+++ b/tests/expr_and_series/over_test.py
@@ -1,8 +1,9 @@
-from typing import Any
+from contextlib import nullcontext as does_not_raise
 
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -12,31 +13,53 @@ data = {
 }
 
 
-def test_over_single(constructor: Any) -> None:
+def test_over_single(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
-    result = df.with_columns(c_max=nw.col("c").max().over("a"))
     expected = {
         "a": ["a", "a", "b", "b", "b"],
         "b": [1, 2, 3, 5, 3],
         "c": [5, 4, 3, 2, 1],
         "c_max": [5, 5, 3, 3, 3],
     }
-    compare_dicts(result, expected)
 
+    context = (
+        pytest.raises(
+            NotImplementedError,
+            match="`Expr.over` is not supported for Dask backend with multiple partitions.",
+        )
+        if "dask_lazy_p2" in str(constructor)
+        else does_not_raise()
+    )
 
-def test_over_multiple(constructor: Any) -> None:
+    with context:
+        result = df.with_columns(c_max=nw.col("c").max().over("a"))
+        compare_dicts(result, expected)
+
+
+def test_over_multiple(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
-    result = df.with_columns(c_min=nw.col("c").min().over("a", "b"))
     expected = {
         "a": ["a", "a", "b", "b", "b"],
         "b": [1, 2, 3, 5, 3],
         "c": [5, 4, 3, 2, 1],
         "c_min": [5, 4, 1, 2, 1],
     }
-    compare_dicts(result, expected)
+
+    context = (
+        pytest.raises(
+            NotImplementedError,
+            match="`Expr.over` is not supported for Dask backend with multiple partitions.",
+        )
+        if "dask_lazy_p2" in str(constructor)
+        else does_not_raise()
+    )
+
+    with context:
+        result = df.with_columns(c_min=nw.col("c").min().over("a", "b"))
+        compare_dicts(result, expected)
 
 
-def test_over_invalid(request: Any, constructor: Any) -> None:
+def test_over_invalid(request: pytest.FixtureRequest, constructor: Constructor) -> None:
     if "polars" in str(constructor):
         request.applymarker(pytest.mark.xfail)
 
diff --git a/tests/expr_and_series/pipe_test.py b/tests/expr_and_series/pipe_test.py
index 55de3548..2134a931 100644
--- a/tests/expr_and_series/pipe_test.py
+++ b/tests/expr_and_series/pipe_test.py
@@ -1,13 +1,14 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 input_list = {"a": [2, 4, 6, 8]}
 expected = [4, 16, 36, 64]
 
 
-def test_pipe_expr(constructor: Any) -> None:
+def test_pipe_expr(constructor: Constructor) -> None:
     df = nw.from_native(constructor(input_list))
     e = df.select(nw.col("a").pipe(lambda x: x**2))
     compare_dicts(e, {"a": expected})
diff --git a/tests/expr_and_series/quantile_test.py b/tests/expr_and_series/quantile_test.py
index d9064541..aae2b364 100644
--- a/tests/expr_and_series/quantile_test.py
+++ b/tests/expr_and_series/quantile_test.py
@@ -1,11 +1,13 @@
 from __future__ import annotations
 
+from contextlib import nullcontext as does_not_raise
 from typing import Any
 from typing import Literal
 
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
@@ -21,19 +23,31 @@ from tests.utils import compare_dicts
 )
 @pytest.mark.filterwarnings("ignore:the `interpolation=` argument to percentile")
 def test_quantile_expr(
-    constructor: Any,
+    constructor: Constructor,
     interpolation: Literal["nearest", "higher", "lower", "midpoint", "linear"],
     expected: dict[str, list[float]],
-    request: Any,
+    request: pytest.FixtureRequest,
 ) -> None:
     if "dask" in str(constructor) and interpolation != "linear":
         request.applymarker(pytest.mark.xfail)
+
     q = 0.3
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
-    result = df.select(nw.all().quantile(quantile=q, interpolation=interpolation))
-    compare_dicts(result, expected)
+
+    context = (
+        pytest.raises(
+            NotImplementedError,
+            match="`Expr.quantile` is not supported for Dask backend with multiple partitions.",
+        )
+        if "dask_lazy_p2" in str(constructor)
+        else does_not_raise()
+    )
+
+    with context:
+        result = df.select(nw.all().quantile(quantile=q, interpolation=interpolation))
+        compare_dicts(result, expected)
 
 
 @pytest.mark.parametrize(
diff --git a/tests/expr_and_series/reduction_test.py b/tests/expr_and_series/reduction_test.py
index 60750444..e22080e6 100644
--- a/tests/expr_and_series/reduction_test.py
+++ b/tests/expr_and_series/reduction_test.py
@@ -5,6 +5,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
@@ -26,7 +27,7 @@ from tests.utils import compare_dicts
     ids=range(5),
 )
 def test_scalar_reduction_select(
-    constructor: Any, expr: list[Any], expected: dict[str, list[Any]]
+    constructor: Constructor, expr: list[Any], expected: dict[str, list[Any]]
 ) -> None:
     data = {"a": [1, 2, 3], "b": [4, 5, 6]}
     df = nw.from_native(constructor(data))
@@ -52,7 +53,7 @@ def test_scalar_reduction_select(
     ids=range(5),
 )
 def test_scalar_reduction_with_columns(
-    constructor: Any, expr: list[Any], expected: dict[str, list[Any]]
+    constructor: Constructor, expr: list[Any], expected: dict[str, list[Any]]
 ) -> None:
     data = {"a": [1, 2, 3], "b": [4, 5, 6]}
     df = nw.from_native(constructor(data))
diff --git a/tests/expr_and_series/round_test.py b/tests/expr_and_series/round_test.py
index 769e4be1..37d6ce13 100644
--- a/tests/expr_and_series/round_test.py
+++ b/tests/expr_and_series/round_test.py
@@ -5,11 +5,12 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("decimals", [0, 1, 2])
-def test_round(constructor: Any, decimals: int) -> None:
+def test_round(constructor: Constructor, decimals: int) -> None:
     data = {"a": [2.12345, 2.56789, 3.901234]}
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
diff --git a/tests/expr_and_series/sample_test.py b/tests/expr_and_series/sample_test.py
index c64703d3..eb6d853e 100644
--- a/tests/expr_and_series/sample_test.py
+++ b/tests/expr_and_series/sample_test.py
@@ -1,11 +1,11 @@
-from typing import Any
-
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
+from tests.utils import compare_dicts
 
 
-def test_expr_sample(constructor: Any, request: Any) -> None:
+def test_expr_sample(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor({"a": [1, 2, 3], "b": [4, 5, 6]})).lazy()
@@ -19,7 +19,9 @@ def test_expr_sample(constructor: Any, request: Any) -> None:
     assert result_series == expected_series
 
 
-def test_expr_sample_fraction(constructor: Any, request: Any) -> None:
+def test_expr_sample_fraction(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor({"a": [1, 2, 3] * 10, "b": [4, 5, 6] * 10})).lazy()
@@ -31,3 +33,37 @@ def test_expr_sample_fraction(constructor: Any, request: Any) -> None:
     result_series = df.collect()["a"].sample(fraction=0.1).shape
     expected_series = (3,)
     assert result_series == expected_series
+
+
+def test_sample_with_seed(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
+    if "dask" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    size, n = 100, 10
+    df = nw.from_native(constructor({"a": list(range(size))})).lazy()
+    expected = {"res1": [True], "res2": [False]}
+    result = (
+        df.select(
+            seed1=nw.col("a").sample(n=n, seed=123),
+            seed2=nw.col("a").sample(n=n, seed=123),
+            seed3=nw.col("a").sample(n=n, seed=42),
+        )
+        .select(
+            res1=(nw.col("seed1") == nw.col("seed2")).all(),
+            res2=(nw.col("seed1") == nw.col("seed3")).all(),
+        )
+        .collect()
+    )
+
+    compare_dicts(result, expected)
+
+    series = df.collect()["a"]
+    seed1 = series.sample(n=n, seed=123)
+    seed2 = series.sample(n=n, seed=123)
+    seed3 = series.sample(n=n, seed=42)
+
+    compare_dicts(
+        {"res1": [(seed1 == seed2).all()], "res2": [(seed1 == seed3).all()]}, expected
+    )
diff --git a/tests/expr_and_series/shift_test.py b/tests/expr_and_series/shift_test.py
index 02dbed6b..b165adf1 100644
--- a/tests/expr_and_series/shift_test.py
+++ b/tests/expr_and_series/shift_test.py
@@ -3,6 +3,7 @@ from typing import Any
 import pyarrow as pa
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -13,7 +14,7 @@ data = {
 }
 
 
-def test_shift(constructor: Any) -> None:
+def test_shift(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.with_columns(nw.col("a", "b", "c").shift(2)).filter(nw.col("i") > 1)
     expected = {
diff --git a/tests/expr_and_series/std_test.py b/tests/expr_and_series/std_test.py
index 0c7e2f33..400a6e0a 100644
--- a/tests/expr_and_series/std_test.py
+++ b/tests/expr_and_series/std_test.py
@@ -1,12 +1,13 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
 
 
-def test_std(constructor: Any) -> None:
+def test_std(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(
         nw.col("a").std().alias("a_ddof_default"),
diff --git a/tests/expr_and_series/str/contains_test.py b/tests/expr_and_series/str/contains_test.py
index 5cc90f4a..4dec60fe 100644
--- a/tests/expr_and_series/str/contains_test.py
+++ b/tests/expr_and_series/str/contains_test.py
@@ -4,6 +4,7 @@ import pandas as pd
 import polars as pl
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"pets": ["cat", "dog", "rabbit and parrot", "dove"]}
@@ -12,7 +13,7 @@ df_pandas = pd.DataFrame(data)
 df_polars = pl.DataFrame(data)
 
 
-def test_contains(constructor: Any) -> None:
+def test_contains(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.with_columns(
         nw.col("pets").str.contains("(?i)parrot|Dove").alias("result")
diff --git a/tests/expr_and_series/str/head_test.py b/tests/expr_and_series/str/head_test.py
index 1160920f..a4b3e729 100644
--- a/tests/expr_and_series/str/head_test.py
+++ b/tests/expr_and_series/str/head_test.py
@@ -1,12 +1,13 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": ["foo", "bars"]}
 
 
-def test_str_head(constructor: Any) -> None:
+def test_str_head(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a").str.head(3))
     expected = {
diff --git a/tests/expr_and_series/str/replace_test.py b/tests/expr_and_series/str/replace_test.py
index 95b5bd87..b0cffb1b 100644
--- a/tests/expr_and_series/str/replace_test.py
+++ b/tests/expr_and_series/str/replace_test.py
@@ -5,6 +5,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 replace_data = [
@@ -92,7 +93,7 @@ def test_str_replace_all_series(
     replace_data,
 )
 def test_str_replace_expr(
-    constructor: Any,
+    constructor: Constructor,
     data: dict[str, list[str]],
     pattern: str,
     value: str,
@@ -113,7 +114,7 @@ def test_str_replace_expr(
     replace_all_data,
 )
 def test_str_replace_all_expr(
-    constructor: Any,
+    constructor: Constructor,
     data: dict[str, list[str]],
     pattern: str,
     value: str,
diff --git a/tests/expr_and_series/str/slice_test.py b/tests/expr_and_series/str/slice_test.py
index e4e7905f..e7fe0efa 100644
--- a/tests/expr_and_series/str/slice_test.py
+++ b/tests/expr_and_series/str/slice_test.py
@@ -5,6 +5,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": ["fdas", "edfas"]}
@@ -15,7 +16,7 @@ data = {"a": ["fdas", "edfas"]}
     [(1, 2, {"a": ["da", "df"]}), (-2, None, {"a": ["as", "as"]})],
 )
 def test_str_slice(
-    constructor: Any, offset: int, length: int | None, expected: Any
+    constructor: Constructor, offset: int, length: int | None, expected: Any
 ) -> None:
     df = nw.from_native(constructor(data))
     result_frame = df.select(nw.col("a").str.slice(offset, length))
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 a5101edc..e8b0afaa 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
@@ -3,6 +3,7 @@ from __future__ import annotations
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 
 # Don't move this into typechecking block, for coverage
 # purposes
@@ -11,7 +12,7 @@ from tests.utils import compare_dicts
 data = {"a": ["fdas", "edfas"]}
 
 
-def test_ends_with(constructor: Any) -> None:
+def test_ends_with(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.col("a").str.ends_with("das"))
     expected = {
@@ -29,7 +30,7 @@ def test_ends_with_series(constructor_eager: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_starts_with(constructor: Any) -> None:
+def test_starts_with(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data)).lazy()
     result = df.select(nw.col("a").str.starts_with("fda"))
     expected = {
diff --git a/tests/expr_and_series/str/strip_chars_test.py b/tests/expr_and_series/str/strip_chars_test.py
index f6cbcc4f..3d5b7445 100644
--- a/tests/expr_and_series/str/strip_chars_test.py
+++ b/tests/expr_and_series/str/strip_chars_test.py
@@ -5,6 +5,7 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": ["foobar", "bar\n", " baz"]}
@@ -17,7 +18,9 @@ data = {"a": ["foobar", "bar\n", " baz"]}
         ("foo", {"a": ["bar", "bar\n", " baz"]}),
     ],
 )
-def test_str_strip_chars(constructor: Any, characters: str | None, expected: Any) -> None:
+def test_str_strip_chars(
+    constructor: Constructor, characters: str | None, expected: Any
+) -> None:
     df = nw.from_native(constructor(data))
     result_frame = df.select(nw.col("a").str.strip_chars(characters))
     compare_dicts(result_frame, expected)
diff --git a/tests/expr_and_series/str/tail_test.py b/tests/expr_and_series/str/tail_test.py
index c863cca0..92d47426 100644
--- a/tests/expr_and_series/str/tail_test.py
+++ b/tests/expr_and_series/str/tail_test.py
@@ -1,12 +1,13 @@
 from typing import Any
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": ["foo", "bars"]}
 
 
-def test_str_tail(constructor: Any) -> None:
+def test_str_tail(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     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 8c3d1a51..ad666aa8 100644
--- a/tests/expr_and_series/str/to_datetime_test.py
+++ b/tests/expr_and_series/str/to_datetime_test.py
@@ -1,11 +1,10 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 
 data = {"a": ["2020-01-01T12:34:56"]}
 
 
-def test_to_datetime(constructor: Any) -> None:
+def test_to_datetime(constructor: Constructor) -> None:
     result = (
         nw.from_native(constructor(data))
         .lazy()
diff --git a/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py b/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py
index 4d2f2f74..87740913 100644
--- a/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py
+++ b/tests/expr_and_series/str/to_uppercase_to_lowercase_test.py
@@ -7,6 +7,7 @@ import pytest
 
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
@@ -26,10 +27,10 @@ from tests.utils import compare_dicts
     ],
 )
 def test_str_to_uppercase(
-    constructor: Any,
+    constructor: Constructor,
     data: dict[str, list[str]],
     expected: dict[str, list[str]],
-    request: Any,
+    request: pytest.FixtureRequest,
 ) -> None:
     df = nw.from_native(constructor(data))
     result_frame = df.select(nw.col("a").str.to_uppercase())
@@ -70,7 +71,7 @@ def test_str_to_uppercase_series(
     constructor_eager: Any,
     data: dict[str, list[str]],
     expected: dict[str, list[str]],
-    request: Any,
+    request: pytest.FixtureRequest,
 ) -> None:
     df = nw.from_native(constructor_eager(data), eager_only=True)
 
@@ -80,6 +81,7 @@ def test_str_to_uppercase_series(
             "pandas_constructor",
             "pandas_nullable_constructor",
             "polars_eager_constructor",
+            "cudf_constructor",
         )
     ):
         # We are marking it xfail for these conditions above
@@ -107,7 +109,7 @@ def test_str_to_uppercase_series(
     ],
 )
 def test_str_to_lowercase(
-    constructor: Any,
+    constructor: Constructor,
     data: dict[str, list[str]],
     expected: dict[str, list[str]],
 ) -> None:
diff --git a/tests/expr_and_series/sum_horizontal_test.py b/tests/expr_and_series/sum_horizontal_test.py
index 4c4ab924..bfaab723 100644
--- a/tests/expr_and_series/sum_horizontal_test.py
+++ b/tests/expr_and_series/sum_horizontal_test.py
@@ -3,11 +3,12 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("col_expr", [nw.col("a"), "a"])
-def test_sumh(constructor: Any, col_expr: Any) -> None:
+def test_sumh(constructor: Constructor, col_expr: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.with_columns(horizontal_sum=nw.sum_horizontal(col_expr, nw.col("b")))
@@ -20,7 +21,7 @@ def test_sumh(constructor: Any, col_expr: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_sumh_nullable(constructor: Any) -> None:
+def test_sumh_nullable(constructor: Constructor) -> None:
     data = {"a": [1, 8, 3], "b": [4, 5, None]}
     expected = {"hsum": [5, 13, 3]}
 
diff --git a/tests/expr_and_series/sum_test.py b/tests/expr_and_series/sum_test.py
index c61a9ed7..8059a097 100644
--- a/tests/expr_and_series/sum_test.py
+++ b/tests/expr_and_series/sum_test.py
@@ -5,13 +5,14 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
 
 
 @pytest.mark.parametrize("expr", [nw.col("a", "b", "z").sum(), nw.sum("a", "b", "z")])
-def test_expr_sum_expr(constructor: Any, expr: nw.Expr) -> None:
+def test_expr_sum_expr(constructor: Constructor, expr: nw.Expr) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(expr)
     expected = {"a": [6], "b": [14], "z": [24.0]}
diff --git a/tests/expr_and_series/tail_test.py b/tests/expr_and_series/tail_test.py
index be17ffb4..fc3e6159 100644
--- a/tests/expr_and_series/tail_test.py
+++ b/tests/expr_and_series/tail_test.py
@@ -1,15 +1,14 @@
-from __future__ import annotations
-
 from typing import Any
 
 import pytest
 
 import narwhals as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
 @pytest.mark.parametrize("n", [2, -1])
-def test_head(constructor: Any, n: int, request: Any) -> None:
+def test_head(constructor: Constructor, n: int, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     if "polars" in str(constructor) and n < 0:
diff --git a/tests/expr_and_series/unary_test.py b/tests/expr_and_series/unary_test.py
index 7df0099d..dabab0c0 100644
--- a/tests/expr_and_series/unary_test.py
+++ b/tests/expr_and_series/unary_test.py
@@ -3,10 +3,11 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_unary(constructor: Any, request: Any) -> None:
+def test_unary(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
diff --git a/tests/expr_and_series/unique_test.py b/tests/expr_and_series/unique_test.py
index 488d793c..5639179b 100644
--- a/tests/expr_and_series/unique_test.py
+++ b/tests/expr_and_series/unique_test.py
@@ -3,12 +3,13 @@ from typing import Any
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 1, 2]}
 
 
-def test_unique_expr(constructor: Any, request: Any) -> None:
+def test_unique_expr(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data))
diff --git a/tests/expr_and_series/when_test.py b/tests/expr_and_series/when_test.py
index e83ec1dd..bcd796a4 100644
--- a/tests/expr_and_series/when_test.py
+++ b/tests/expr_and_series/when_test.py
@@ -6,6 +6,7 @@ import numpy as np
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -17,7 +18,7 @@ data = {
 }
 
 
-def test_when(constructor: Any) -> None:
+def test_when(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") == 1).then(value=3).alias("a_when"))
     expected = {
@@ -26,7 +27,7 @@ def test_when(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_when_otherwise(constructor: Any) -> None:
+def test_when_otherwise(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") == 1).then(3).otherwise(6).alias("a_when"))
     expected = {
@@ -35,7 +36,7 @@ def test_when_otherwise(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_multiple_conditions(constructor: Any) -> None:
+def test_multiple_conditions(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(
         nw.when(nw.col("a") < 3, nw.col("c") < 5.0).then(3).alias("a_when")
@@ -46,13 +47,15 @@ def test_multiple_conditions(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_no_arg_when_fail(constructor: Any) -> None:
+def test_no_arg_when_fail(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     with pytest.raises((TypeError, ValueError)):
         df.select(nw.when().then(value=3).alias("a_when"))
 
 
-def test_value_numpy_array(request: Any, constructor: Any) -> None:
+def test_value_numpy_array(
+    request: pytest.FixtureRequest, constructor: Constructor
+) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
 
@@ -80,7 +83,7 @@ def test_value_series(constructor_eager: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_value_expression(constructor: Any) -> None:
+def test_value_expression(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") == 1).then(nw.col("a") + 9).alias("a_when"))
     expected = {
@@ -89,7 +92,9 @@ def test_value_expression(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_otherwise_numpy_array(request: Any, constructor: Any) -> None:
+def test_otherwise_numpy_array(
+    request: pytest.FixtureRequest, constructor: Constructor
+) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
 
@@ -117,7 +122,9 @@ def test_otherwise_series(constructor_eager: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_otherwise_expression(request: Any, constructor: Any) -> None:
+def test_otherwise_expression(
+    request: pytest.FixtureRequest, constructor: Constructor
+) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
 
@@ -131,7 +138,9 @@ def test_otherwise_expression(request: Any, constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_when_then_otherwise_into_expr(request: Any, constructor: Any) -> None:
+def test_when_then_otherwise_into_expr(
+    request: pytest.FixtureRequest, constructor: Constructor
+) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
 
diff --git a/tests/frame/add_test.py b/tests/frame/add_test.py
index b885bb05..c95fbae9 100644
--- a/tests/frame/add_test.py
+++ b/tests/frame/add_test.py
@@ -1,10 +1,9 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_add(constructor: Any) -> None:
+def test_add(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.with_columns(
diff --git a/tests/frame/array_dunder_test.py b/tests/frame/array_dunder_test.py
index 8f9cbd16..8a082bb1 100644
--- a/tests/frame/array_dunder_test.py
+++ b/tests/frame/array_dunder_test.py
@@ -11,7 +11,7 @@ from narwhals.utils import parse_version
 from tests.utils import compare_dicts
 
 
-def test_array_dunder(request: Any, constructor_eager: Any) -> None:
+def test_array_dunder(request: pytest.FixtureRequest, constructor_eager: Any) -> None:
     if "pyarrow_table" in str(constructor_eager) and parse_version(
         pa.__version__
     ) < parse_version("16.0.0"):  # pragma: no cover
@@ -22,7 +22,9 @@ def test_array_dunder(request: Any, constructor_eager: Any) -> None:
     np.testing.assert_array_equal(result, np.array([[1], [2], [3]], dtype="int64"))
 
 
-def test_array_dunder_with_dtype(request: Any, constructor_eager: Any) -> None:
+def test_array_dunder_with_dtype(
+    request: pytest.FixtureRequest, constructor_eager: Any
+) -> None:
     if "pyarrow_table" in str(constructor_eager) and parse_version(
         pa.__version__
     ) < parse_version("16.0.0"):  # pragma: no cover
@@ -33,7 +35,9 @@ def test_array_dunder_with_dtype(request: Any, constructor_eager: Any) -> None:
     np.testing.assert_array_equal(result, np.array([[1], [2], [3]], dtype=object))
 
 
-def test_array_dunder_with_copy(request: Any, constructor_eager: Any) -> None:
+def test_array_dunder_with_copy(
+    request: pytest.FixtureRequest, constructor_eager: Any
+) -> None:
     if "pyarrow_table" in str(constructor_eager) and parse_version(pa.__version__) < (
         16,
         0,
diff --git a/tests/frame/clone_test.py b/tests/frame/clone_test.py
index 6e8b19be..e94183e2 100644
--- a/tests/frame/clone_test.py
+++ b/tests/frame/clone_test.py
@@ -1,12 +1,11 @@
-from typing import Any
-
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_clone(request: Any, constructor: Any) -> None:
+def test_clone(request: pytest.FixtureRequest, constructor: Constructor) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     if "pyarrow_table" in str(constructor):
diff --git a/tests/frame/columns_test.py b/tests/frame/columns_test.py
index 157051ba..90a9c922 100644
--- a/tests/frame/columns_test.py
+++ b/tests/frame/columns_test.py
@@ -1,12 +1,11 @@
-from typing import Any
-
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 
 
 @pytest.mark.filterwarnings("ignore:Determining|Resolving.*")
-def test_columns(constructor: Any) -> None:
+def test_columns(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.columns
diff --git a/tests/frame/concat_test.py b/tests/frame/concat_test.py
index a5275912..926f3f98 100644
--- a/tests/frame/concat_test.py
+++ b/tests/frame/concat_test.py
@@ -1,12 +1,11 @@
-from typing import Any
-
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_concat_horizontal(constructor: Any) -> None:
+def test_concat_horizontal(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df_left = nw.from_native(constructor(data)).lazy()
 
@@ -27,7 +26,7 @@ def test_concat_horizontal(constructor: Any) -> None:
         nw.concat([])
 
 
-def test_concat_vertical(constructor: Any) -> None:
+def test_concat_vertical(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df_left = (
         nw.from_native(constructor(data)).lazy().rename({"a": "c", "b": "d"}).drop("z")
diff --git a/tests/frame/double_test.py b/tests/frame/double_test.py
index 5d52d0d2..6840145e 100644
--- a/tests/frame/double_test.py
+++ b/tests/frame/double_test.py
@@ -1,10 +1,9 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_double(constructor: Any) -> None:
+def test_double(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
 
diff --git a/tests/frame/drop_nulls_test.py b/tests/frame/drop_nulls_test.py
index 58c9486e..4c227603 100644
--- a/tests/frame/drop_nulls_test.py
+++ b/tests/frame/drop_nulls_test.py
@@ -1,10 +1,9 @@
 from __future__ import annotations
 
-from typing import Any
-
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -13,7 +12,7 @@ data = {
 }
 
 
-def test_drop_nulls(constructor: Any) -> None:
+def test_drop_nulls(constructor: Constructor) -> None:
     result = nw.from_native(constructor(data)).drop_nulls()
     expected = {
         "a": [2.0, 4.0],
@@ -23,7 +22,7 @@ def test_drop_nulls(constructor: Any) -> None:
 
 
 @pytest.mark.parametrize("subset", ["a", ["a"]])
-def test_drop_nulls_subset(constructor: Any, subset: str | list[str]) -> None:
+def test_drop_nulls_subset(constructor: Constructor, subset: str | list[str]) -> None:
     result = nw.from_native(constructor(data)).drop_nulls(subset=subset)
     expected = {
         "a": [1, 2.0, 4.0],
diff --git a/tests/frame/drop_test.py b/tests/frame/drop_test.py
index db039fcb..f8fc3325 100644
--- a/tests/frame/drop_test.py
+++ b/tests/frame/drop_test.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from contextlib import nullcontext as does_not_raise
+from typing import TYPE_CHECKING
 from typing import Any
 
 import polars as pl
@@ -11,6 +12,9 @@ import narwhals.stable.v1 as nw
 from narwhals._exceptions import ColumnNotFoundError
 from narwhals.utils import parse_version
 
+if TYPE_CHECKING:
+    from tests.utils import Constructor
+
 
 @pytest.mark.parametrize(
     ("to_drop", "expected"),
@@ -20,7 +24,7 @@ from narwhals.utils import parse_version
         (["abc", "b"], ["z"]),
     ],
 )
-def test_drop(constructor: Any, to_drop: list[str], expected: list[str]) -> None:
+def test_drop(constructor: Constructor, to_drop: list[str], expected: list[str]) -> None:
     data = {"abc": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     assert df.drop(to_drop).collect_schema().names() == expected
@@ -38,7 +42,13 @@ def test_drop(constructor: Any, to_drop: list[str], expected: list[str]) -> None
         (False, does_not_raise()),
     ],
 )
-def test_drop_strict(request: Any, constructor: Any, strict: bool, context: Any) -> None:  # noqa: FBT001
+def test_drop_strict(
+    request: pytest.FixtureRequest,
+    constructor: Constructor,
+    context: Any,
+    *,
+    strict: bool,
+) -> None:
     if (
         "polars_lazy" in str(request)
         and parse_version(pl.__version__) < (1, 0, 0)
diff --git a/tests/frame/filter_test.py b/tests/frame/filter_test.py
index a8d3144a..9c9b1b6f 100644
--- a/tests/frame/filter_test.py
+++ b/tests/frame/filter_test.py
@@ -1,10 +1,13 @@
-from typing import Any
+from contextlib import nullcontext as does_not_raise
+
+import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_filter(constructor: Any) -> None:
+def test_filter(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.filter(nw.col("a") > 1)
@@ -12,9 +15,20 @@ def test_filter(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_filter_with_boolean_list(constructor: Any) -> None:
+def test_filter_with_boolean_list(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
-    result = df.filter([False, True, True])
-    expected = {"a": [3, 2], "b": [4, 6], "z": [8.0, 9.0]}
-    compare_dicts(result, expected)
+
+    context = (
+        pytest.raises(
+            NotImplementedError,
+            match="`LazyFrame.filter` is not supported for Dask backend with boolean masks.",
+        )
+        if "dask" in str(constructor)
+        else does_not_raise()
+    )
+
+    with context:
+        result = df.filter([False, True, True])
+        expected = {"a": [3, 2], "b": [4, 6], "z": [8.0, 9.0]}
+        compare_dicts(result, expected)
diff --git a/tests/frame/gather_every_test.py b/tests/frame/gather_every_test.py
index 90b06e3d..40e18a30 100644
--- a/tests/frame/gather_every_test.py
+++ b/tests/frame/gather_every_test.py
@@ -1,8 +1,7 @@
-from typing import Any
-
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": list(range(10))}
@@ -10,7 +9,7 @@ data = {"a": list(range(10))}
 
 @pytest.mark.parametrize("n", [1, 2, 3])
 @pytest.mark.parametrize("offset", [1, 2, 3])
-def test_gather_every(constructor: Any, n: int, offset: int) -> None:
+def test_gather_every(constructor: Constructor, n: int, offset: int) -> None:
     df = nw.from_native(constructor(data))
     result = df.gather_every(n=n, offset=offset)
     expected = {"a": data["a"][offset::n]}
diff --git a/tests/frame/head_test.py b/tests/frame/head_test.py
index e4b762f4..7234828b 100644
--- a/tests/frame/head_test.py
+++ b/tests/frame/head_test.py
@@ -1,12 +1,11 @@
 from __future__ import annotations
 
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_head(constructor: Any) -> None:
+def test_head(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     expected = {"a": [1, 3], "b": [4, 4], "z": [7.0, 8.0]}
 
diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py
index 18e9aae6..85c76eba 100644
--- a/tests/frame/join_test.py
+++ b/tests/frame/join_test.py
@@ -11,10 +11,11 @@ import pytest
 import narwhals.stable.v1 as nw
 from narwhals.utils import Implementation
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_inner_join_two_keys(constructor: Any) -> None:
+def test_inner_join_two_keys(constructor: Constructor) -> None:
     data = {
         "antananarivo": [1, 3, 2],
         "bob": [4, 4, 6],
@@ -43,7 +44,7 @@ def test_inner_join_two_keys(constructor: Any) -> None:
     compare_dicts(result_on, expected)
 
 
-def test_inner_join_single_key(constructor: Any) -> None:
+def test_inner_join_single_key(constructor: Constructor) -> None:
     data = {
         "antananarivo": [1, 3, 2],
         "bob": [4, 4, 6],
@@ -73,7 +74,7 @@ def test_inner_join_single_key(constructor: Any) -> None:
     compare_dicts(result_on, expected)
 
 
-def test_cross_join(constructor: Any) -> None:
+def test_cross_join(constructor: Constructor) -> None:
     data = {"antananarivo": [1, 3, 2]}
     df = nw.from_native(constructor(data))
     result = df.join(df, how="cross").sort("antananarivo", "antananarivo_right")  # type: ignore[arg-type]
@@ -91,7 +92,7 @@ def test_cross_join(constructor: Any) -> None:
 
 @pytest.mark.parametrize("how", ["inner", "left"])
 @pytest.mark.parametrize("suffix", ["_right", "_custom_suffix"])
-def test_suffix(constructor: Any, how: str, suffix: str) -> None:
+def test_suffix(constructor: Constructor, how: str, suffix: str) -> None:
     data = {
         "antananarivo": [1, 3, 2],
         "bob": [4, 4, 6],
@@ -111,7 +112,7 @@ def test_suffix(constructor: Any, how: str, suffix: str) -> None:
 
 
 @pytest.mark.parametrize("suffix", ["_right", "_custom_suffix"])
-def test_cross_join_suffix(constructor: Any, suffix: str) -> None:
+def test_cross_join_suffix(constructor: Constructor, suffix: str) -> None:
     data = {"antananarivo": [1, 3, 2]}
     df = nw.from_native(constructor(data))
     result = df.join(df, how="cross", suffix=suffix).sort(  # type: ignore[arg-type]
@@ -154,7 +155,7 @@ def test_cross_join_non_pandas() -> None:
     ],
 )
 def test_anti_join(
-    constructor: Any,
+    constructor: Constructor,
     join_key: list[str],
     filter_expr: nw.Expr,
     expected: dict[str, list[Any]],
@@ -169,6 +170,11 @@ def test_anti_join(
 @pytest.mark.parametrize(
     ("join_key", "filter_expr", "expected"),
     [
+        (
+            "antananarivo",
+            (nw.col("bob") > 5),
+            {"antananarivo": [2], "bob": [6], "zorro": [9]},
+        ),
         (
             ["antananarivo"],
             (nw.col("bob") > 5),
@@ -187,7 +193,7 @@ def test_anti_join(
     ],
 )
 def test_semi_join(
-    constructor: Any,
+    constructor: Constructor,
     join_key: list[str],
     filter_expr: nw.Expr,
     expected: dict[str, list[Any]],
@@ -202,7 +208,7 @@ def test_semi_join(
 
 
 @pytest.mark.parametrize("how", ["right", "full"])
-def test_join_not_implemented(constructor: Any, how: str) -> None:
+def test_join_not_implemented(constructor: Constructor, how: str) -> None:
     data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zorro": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
 
@@ -216,16 +222,20 @@ def test_join_not_implemented(constructor: Any, how: str) -> None:
 
 
 @pytest.mark.filterwarnings("ignore:the default coalesce behavior")
-def test_left_join(constructor: Any) -> None:
+def test_left_join(constructor: Constructor) -> None:
     data_left = {
         "antananarivo": [1.0, 2, 3],
         "bob": [4.0, 5, 6],
         "index": [0.0, 1.0, 2.0],
     }
-    data_right = {"antananarivo": [1.0, 2, 3], "c": [4.0, 5, 7], "index": [0.0, 1.0, 2.0]}
+    data_right = {
+        "antananarivo": [1.0, 2, 3],
+        "co": [4.0, 5, 7],
+        "index": [0.0, 1.0, 2.0],
+    }
     df_left = nw.from_native(constructor(data_left))
     df_right = nw.from_native(constructor(data_right))
-    result = df_left.join(df_right, left_on="bob", right_on="c", how="left").select(  # type: ignore[arg-type]
+    result = df_left.join(df_right, left_on="bob", right_on="co", how="left").select(  # type: ignore[arg-type]
         nw.all().fill_null(float("nan"))
     )
     result = result.sort("index")
@@ -236,11 +246,24 @@ def test_left_join(constructor: Any) -> None:
         "antananarivo_right": [1, 2, float("nan")],
         "index": [0, 1, 2],
     }
+    result_on_list = df_left.join(
+        df_right,  # type: ignore[arg-type]
+        on=["antananarivo", "index"],
+        how="left",
+    ).select(nw.all().fill_null(float("nan")))
+    result_on_list = result_on_list.sort("index")
+    expected_on_list = {
+        "antananarivo": [1, 2, 3],
+        "bob": [4, 5, 6],
+        "index": [0, 1, 2],
+        "co": [4, 5, 7],
+    }
     compare_dicts(result, expected)
+    compare_dicts(result_on_list, expected_on_list)
 
 
 @pytest.mark.filterwarnings("ignore: the default coalesce behavior")
-def test_left_join_multiple_column(constructor: Any) -> None:
+def test_left_join_multiple_column(constructor: Constructor) -> None:
     data_left = {"antananarivo": [1, 2, 3], "bob": [4, 5, 6], "index": [0, 1, 2]}
     data_right = {"antananarivo": [1, 2, 3], "c": [4, 5, 6], "index": [0, 1, 2]}
     df_left = nw.from_native(constructor(data_left))
@@ -258,7 +281,7 @@ def test_left_join_multiple_column(constructor: Any) -> None:
 
 
 @pytest.mark.filterwarnings("ignore: the default coalesce behavior")
-def test_left_join_overlapping_column(constructor: Any) -> None:
+def test_left_join_overlapping_column(constructor: Constructor) -> None:
     data_left = {
         "antananarivo": [1.0, 2, 3],
         "bob": [4.0, 5, 6],
@@ -304,7 +327,7 @@ def test_left_join_overlapping_column(constructor: Any) -> None:
 
 
 @pytest.mark.parametrize("how", ["inner", "left", "semi", "anti"])
-def test_join_keys_exceptions(constructor: Any, how: str) -> None:
+def test_join_keys_exceptions(constructor: Constructor, how: str) -> None:
     data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zorro": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
 
@@ -330,8 +353,10 @@ def test_join_keys_exceptions(constructor: Any, how: str) -> None:
         df.join(df, how=how, on="antananarivo", right_on="antananarivo")  # type: ignore[arg-type]
 
 
-def test_joinasof_numeric(constructor: Any, request: Any) -> None:
-    if "pyarrow_table" in str(constructor):
+def test_joinasof_numeric(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
+    if "pyarrow_table" in str(constructor) or "cudf" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     if parse_version(pd.__version__) < (2, 1) and (
         ("pandas_pyarrow" in str(constructor)) or ("pandas_nullable" in str(constructor))
@@ -386,8 +411,8 @@ def test_joinasof_numeric(constructor: Any, request: Any) -> None:
     compare_dicts(result_nearest_on, expected_nearest)
 
 
-def test_joinasof_time(constructor: Any, request: Any) -> None:
-    if "pyarrow_table" in str(constructor):
+def test_joinasof_time(constructor: Constructor, request: pytest.FixtureRequest) -> None:
+    if "pyarrow_table" in str(constructor) or "cudf" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     if parse_version(pd.__version__) < (2, 1) and ("pandas_pyarrow" in str(constructor)):
         request.applymarker(pytest.mark.xfail)
@@ -464,8 +489,8 @@ def test_joinasof_time(constructor: Any, request: Any) -> None:
     compare_dicts(result_nearest_on, expected_nearest)
 
 
-def test_joinasof_by(constructor: Any, request: Any) -> None:
-    if "pyarrow_table" in str(constructor):
+def test_joinasof_by(constructor: Constructor, request: pytest.FixtureRequest) -> None:
+    if "pyarrow_table" in str(constructor) or "cudf" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     if parse_version(pd.__version__) < (2, 1) and (
         ("pandas_pyarrow" in str(constructor)) or ("pandas_nullable" in str(constructor))
@@ -499,7 +524,7 @@ def test_joinasof_by(constructor: Any, request: Any) -> None:
 
 @pytest.mark.parametrize("strategy", ["back", "furthest"])
 def test_joinasof_not_implemented(
-    constructor: Any, strategy: Literal["backward", "forward"]
+    constructor: Constructor, strategy: Literal["backward", "forward"]
 ) -> None:
     data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zorro": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
@@ -516,7 +541,7 @@ def test_joinasof_not_implemented(
         )
 
 
-def test_joinasof_keys_exceptions(constructor: Any) -> None:
+def test_joinasof_keys_exceptions(constructor: Constructor) -> None:
     data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zorro": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
 
@@ -557,7 +582,7 @@ def test_joinasof_keys_exceptions(constructor: Any) -> None:
         df.join_asof(df, right_on="antananarivo", on="antananarivo")  # type: ignore[arg-type]
 
 
-def test_joinasof_by_exceptions(constructor: Any) -> None:
+def test_joinasof_by_exceptions(constructor: Constructor) -> None:
     data = {"antananarivo": [1, 3, 2], "bob": [4, 4, 6], "zorro": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     with pytest.raises(
diff --git a/tests/frame/lit_test.py b/tests/frame/lit_test.py
index e5756e03..aa18edb4 100644
--- a/tests/frame/lit_test.py
+++ b/tests/frame/lit_test.py
@@ -7,6 +7,7 @@ import numpy as np
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 if TYPE_CHECKING:
@@ -17,7 +18,9 @@ if TYPE_CHECKING:
     ("dtype", "expected_lit"),
     [(None, [2, 2, 2]), (nw.String, ["2", "2", "2"]), (nw.Float32, [2.0, 2.0, 2.0])],
 )
-def test_lit(constructor: Any, dtype: DType | None, expected_lit: list[Any]) -> None:
+def test_lit(
+    constructor: Constructor, dtype: DType | None, expected_lit: list[Any]
+) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df_raw = constructor(data)
     df = nw.from_native(df_raw).lazy()
@@ -31,7 +34,7 @@ def test_lit(constructor: Any, dtype: DType | None, expected_lit: list[Any]) ->
     compare_dicts(result, expected)
 
 
-def test_lit_error(constructor: Any) -> None:
+def test_lit_error(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df_raw = constructor(data)
     df = nw.from_native(df_raw).lazy()
diff --git a/tests/frame/pipe_test.py b/tests/frame/pipe_test.py
index a9a50133..b7b57e0a 100644
--- a/tests/frame/pipe_test.py
+++ b/tests/frame/pipe_test.py
@@ -1,6 +1,5 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -9,7 +8,7 @@ data = {
 }
 
 
-def test_pipe(constructor: Any) -> None:
+def test_pipe(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     columns = df.collect_schema().names()
     result = df.pipe(lambda _df: _df.select([x for x in columns if len(x) == 2]))
diff --git a/tests/frame/rename_test.py b/tests/frame/rename_test.py
index c58eccd4..79cf3f24 100644
--- a/tests/frame/rename_test.py
+++ b/tests/frame/rename_test.py
@@ -1,10 +1,9 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_rename(constructor: Any) -> None:
+def test_rename(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.rename({"a": "x", "b": "y"})
diff --git a/tests/frame/row_test.py b/tests/frame/row_test.py
index 602c50f5..599dcaea 100644
--- a/tests/frame/row_test.py
+++ b/tests/frame/row_test.py
@@ -1,9 +1,14 @@
 from typing import Any
 
+import pytest
+
 import narwhals.stable.v1 as nw
 
 
-def test_row_column(constructor_eager: Any) -> None:
+def test_row_column(request: Any, constructor_eager: Any) -> None:
+    if "cudf" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
+
     data = {
         "a": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
         "b": [11, 12, 13, 14, 15, 16],
diff --git a/tests/frame/rows_test.py b/tests/frame/rows_test.py
index e3123f19..2d94ab18 100644
--- a/tests/frame/rows_test.py
+++ b/tests/frame/rows_test.py
@@ -55,10 +55,14 @@ df_polars_na = pl.DataFrame({"a": [None, 3, 2], "b": [4, 4, 6], "z": [7.0, None,
     ],
 )
 def test_iter_rows(
+    request: Any,
     constructor_eager: Any,
     named: bool,  # noqa: FBT001
     expected: list[tuple[Any, ...]] | list[dict[str, Any]],
 ) -> None:
+    if "cudf" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
+
     data = {"a": [1, 3, 2], "_b": [4, 4, 6], "z": [7.0, 8, 9], "1": [5, 6, 7]}
     df = nw.from_native(constructor_eager(data), eager_only=True)
     result = list(df.iter_rows(named=named))
diff --git a/tests/frame/sample_test.py b/tests/frame/sample_test.py
new file mode 100644
index 00000000..88d5969c
--- /dev/null
+++ b/tests/frame/sample_test.py
@@ -0,0 +1,34 @@
+import narwhals.stable.v1 as nw
+from tests.utils import Constructor
+
+
+def test_sample_n(constructor_eager: Constructor) -> None:
+    df = nw.from_native(
+        constructor_eager({"a": [1, 2, 3, 4], "b": ["x", "y", "x", "y"]}), eager_only=True
+    )
+
+    result_expr = df.sample(n=2).shape
+    expected_expr = (2, 2)
+    assert result_expr == expected_expr
+
+
+def test_sample_fraction(constructor_eager: Constructor) -> None:
+    df = nw.from_native(
+        constructor_eager({"a": [1, 2, 3, 4], "b": ["x", "y", "x", "y"]}), eager_only=True
+    )
+
+    result_expr = df.sample(fraction=0.5).shape
+    expected_expr = (2, 2)
+    assert result_expr == expected_expr
+
+
+def test_sample_with_seed(constructor_eager: Constructor) -> None:
+    size, n = 100, 10
+    df = nw.from_native(constructor_eager({"a": range(size)}), eager_only=True)
+
+    r1 = nw.to_native(df.sample(n=n, seed=123))
+    r2 = nw.to_native(df.sample(n=n, seed=123))
+    r3 = nw.to_native(df.sample(n=n, seed=42))
+
+    assert r1.equals(r2)  # type: ignore[union-attr]
+    assert not r1.equals(r3)  # type: ignore[union-attr]
diff --git a/tests/frame/schema_test.py b/tests/frame/schema_test.py
index 6e6b33aa..d7ba69ab 100644
--- a/tests/frame/schema_test.py
+++ b/tests/frame/schema_test.py
@@ -10,6 +10,7 @@ import pytest
 
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 
 data = {
     "a": [datetime(2020, 1, 1)],
@@ -18,7 +19,7 @@ data = {
 
 
 @pytest.mark.filterwarnings("ignore:Determining|Resolving.*")
-def test_schema(constructor: Any) -> None:
+def test_schema(constructor: Constructor) -> None:
     df = nw.from_native(constructor({"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8, 9]}))
     result = df.schema
     expected = {"a": nw.Int64, "b": nw.Int64, "z": nw.Float64}
@@ -29,7 +30,7 @@ def test_schema(constructor: Any) -> None:
     assert result == expected
 
 
-def test_collect_schema(constructor: Any) -> None:
+def test_collect_schema(constructor: Constructor) -> None:
     df = nw.from_native(constructor({"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8, 9]}))
     expected = {"a": nw.Int64, "b": nw.Int64, "z": nw.Float64}
 
@@ -58,7 +59,7 @@ def test_string_disguised_as_object() -> None:
     assert result["a"] == nw.String
 
 
-def test_actual_object(request: Any, constructor_eager: Any) -> None:
+def test_actual_object(request: pytest.FixtureRequest, constructor_eager: Any) -> None:
     if any(x in str(constructor_eager) for x in ("modin", "pyarrow_table")):
         request.applymarker(pytest.mark.xfail)
 
diff --git a/tests/frame/select_test.py b/tests/frame/select_test.py
index 450e9106..8c01be40 100644
--- a/tests/frame/select_test.py
+++ b/tests/frame/select_test.py
@@ -1,13 +1,12 @@
-from typing import Any
-
 import pandas as pd
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_select(constructor: Any) -> None:
+def test_select(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.select("a")
@@ -15,7 +14,7 @@ def test_select(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_empty_select(constructor: Any) -> None:
+def test_empty_select(constructor: Constructor) -> None:
     result = nw.from_native(constructor({"a": [1, 2, 3]})).lazy().select()
     assert result.collect().shape == (0, 0)
 
diff --git a/tests/frame/slice_test.py b/tests/frame/slice_test.py
index 18b05bf3..0867844f 100644
--- a/tests/frame/slice_test.py
+++ b/tests/frame/slice_test.py
@@ -29,7 +29,9 @@ def test_slice_rows(constructor_eager: Any) -> None:
     compare_dicts(result, {"a": [3.0, 4.0], "b": [13, 14]})
 
 
-def test_slice_rows_with_step(request: Any, constructor_eager: Any) -> None:
+def test_slice_rows_with_step(
+    request: pytest.FixtureRequest, constructor_eager: Any
+) -> None:
     if "pyarrow_table" in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
     result = nw.from_native(constructor_eager(data))[1::2]
@@ -147,6 +149,12 @@ def test_slice_slice_columns(constructor_eager: Any) -> None:
     result = df[:, [0, 2]]
     expected = {"a": [1, 2, 3], "c": [7, 8, 9]}
     compare_dicts(result, expected)
+    result = df[:2, [0, 2]]
+    expected = {"a": [1, 2], "c": [7, 8]}
+    compare_dicts(result, expected)
+    result = df[["b", "c"]]
+    expected = {"b": [4, 5, 6], "c": [7, 8, 9]}
+    compare_dicts(result, expected)
 
 
 def test_slice_invalid(constructor_eager: Any) -> None:
diff --git a/tests/frame/sort_test.py b/tests/frame/sort_test.py
index 9e583f8b..06f5d079 100644
--- a/tests/frame/sort_test.py
+++ b/tests/frame/sort_test.py
@@ -1,10 +1,9 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_sort(constructor: Any) -> None:
+def test_sort(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.sort("a", "b")
diff --git a/tests/frame/tail_test.py b/tests/frame/tail_test.py
index e279caba..f7e06475 100644
--- a/tests/frame/tail_test.py
+++ b/tests/frame/tail_test.py
@@ -1,26 +1,39 @@
 from __future__ import annotations
 
-from typing import Any
+from contextlib import nullcontext as does_not_raise
+
+import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_tail(constructor: Any) -> None:
+def test_tail(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     expected = {"a": [3, 2], "b": [4, 6], "z": [8.0, 9]}
 
     df_raw = constructor(data)
     df = nw.from_native(df_raw).lazy()
 
-    result = df.tail(2)
-    compare_dicts(result, expected)
+    context = (
+        pytest.raises(
+            NotImplementedError,
+            match="`LazyFrame.tail` is not supported for Dask backend with multiple partitions.",
+        )
+        if "dask_lazy_p2" in str(constructor)
+        else does_not_raise()
+    )
+
+    with context:
+        result = df.tail(2)
+        compare_dicts(result, expected)
 
-    result = df.collect().tail(2)  # type: ignore[assignment]
-    compare_dicts(result, expected)
+        result = df.collect().tail(2)  # type: ignore[assignment]
+        compare_dicts(result, expected)
 
-    result = df.collect().tail(-1)  # type: ignore[assignment]
-    compare_dicts(result, expected)
+        result = df.collect().tail(-1)  # type: ignore[assignment]
+        compare_dicts(result, expected)
 
-    result = df.collect().select(nw.col("a").tail(2))  # type: ignore[assignment]
-    compare_dicts(result, {"a": expected["a"]})
+        result = df.collect().select(nw.col("a").tail(2))  # type: ignore[assignment]
+        compare_dicts(result, {"a": expected["a"]})
diff --git a/tests/frame/to_arrow_test.py b/tests/frame/to_arrow_test.py
index c1f395e5..f20bdf28 100644
--- a/tests/frame/to_arrow_test.py
+++ b/tests/frame/to_arrow_test.py
@@ -10,7 +10,7 @@ import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
 
 
-def test_to_arrow(request: Any, constructor_eager: Any) -> None:
+def test_to_arrow(request: pytest.FixtureRequest, constructor_eager: Any) -> None:
     if "pandas" in str(constructor_eager) and parse_version(pd.__version__) < (1, 0, 0):
         # pyarrow requires pandas>=1.0.0
         request.applymarker(pytest.mark.xfail)
diff --git a/tests/frame/to_pandas_test.py b/tests/frame/to_pandas_test.py
index 81685a60..671a5d85 100644
--- a/tests/frame/to_pandas_test.py
+++ b/tests/frame/to_pandas_test.py
@@ -14,7 +14,7 @@ from narwhals.utils import parse_version
     parse_version(pd.__version__) < parse_version("2.0.0"),
     reason="too old for pandas-pyarrow",
 )
-def test_convert_pandas(constructor_eager: Any, request: Any) -> None:
+def test_convert_pandas(constructor_eager: Any, request: pytest.FixtureRequest) -> None:
     if "modin" in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
diff --git a/tests/frame/unique_test.py b/tests/frame/unique_test.py
index af61fe82..40589c54 100644
--- a/tests/frame/unique_test.py
+++ b/tests/frame/unique_test.py
@@ -1,10 +1,9 @@
 from __future__ import annotations
 
-from typing import Any
-
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
@@ -21,7 +20,7 @@ data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     ],
 )
 def test_unique(
-    constructor: Any,
+    constructor: Constructor,
     subset: str | list[str] | None,
     keep: str,
     expected: dict[str, list[float]],
@@ -33,7 +32,7 @@ def test_unique(
     compare_dicts(result, expected)
 
 
-def test_unique_none(constructor: Any) -> None:
+def test_unique_none(constructor: Constructor) -> None:
     df_raw = constructor(data)
     df = nw.from_native(df_raw)
 
diff --git a/tests/frame/with_columns_sequence_test.py b/tests/frame/with_columns_sequence_test.py
index 12342512..49db7820 100644
--- a/tests/frame/with_columns_sequence_test.py
+++ b/tests/frame/with_columns_sequence_test.py
@@ -1,9 +1,8 @@
-from typing import Any
-
 import numpy as np
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -12,7 +11,7 @@ data = {
 }
 
 
-def test_with_columns(constructor: Any, request: Any) -> None:
+def test_with_columns(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     result = (
diff --git a/tests/frame/with_columns_test.py b/tests/frame/with_columns_test.py
index 864e689e..44bcd39a 100644
--- a/tests/frame/with_columns_test.py
+++ b/tests/frame/with_columns_test.py
@@ -1,9 +1,8 @@
-from typing import Any
-
 import numpy as np
 import pandas as pd
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
@@ -18,7 +17,7 @@ def test_with_columns_int_col_name_pandas() -> None:
     pd.testing.assert_frame_equal(result, expected)
 
 
-def test_with_columns_order(constructor: Any) -> None:
+def test_with_columns_order(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.with_columns(nw.col("a") + 1, d=nw.col("a") - 1)
@@ -27,14 +26,14 @@ def test_with_columns_order(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_with_columns_empty(constructor: Any) -> None:
+def test_with_columns_empty(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     result = df.select().with_columns()
     compare_dicts(result, {})
 
 
-def test_with_columns_order_single_row(constructor: Any) -> None:
+def test_with_columns_order_single_row(constructor: Constructor) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9], "i": [0, 1, 2]}
     df = nw.from_native(constructor(data)).filter(nw.col("i") < 1).drop("i")
     result = df.with_columns(nw.col("a") + 1, d=nw.col("a") - 1)
diff --git a/tests/frame/with_row_index_test.py b/tests/frame/with_row_index_test.py
index bc1c2fe0..8f802de0 100644
--- a/tests/frame/with_row_index_test.py
+++ b/tests/frame/with_row_index_test.py
@@ -1,6 +1,5 @@
-from typing import Any
-
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -9,7 +8,7 @@ data = {
 }
 
 
-def test_with_row_index(constructor: Any) -> None:
+def test_with_row_index(constructor: Constructor) -> None:
     result = nw.from_native(constructor(data)).with_row_index()
     expected = {"a": ["foo", "bars"], "ab": ["foo", "bars"], "index": [0, 1]}
     compare_dicts(result, expected)
diff --git a/tests/from_dict_test.py b/tests/from_dict_test.py
index cfaf99a7..a1332908 100644
--- a/tests/from_dict_test.py
+++ b/tests/from_dict_test.py
@@ -1,12 +1,11 @@
-from typing import Any
-
 import pytest
 
 import narwhals.stable.v1 as nw
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_from_dict(constructor: Any, request: Any) -> None:
+def test_from_dict(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor({"a": [1, 2, 3], "b": [4, 5, 6]}))
@@ -17,7 +16,9 @@ def test_from_dict(constructor: Any, request: Any) -> None:
     assert isinstance(result, nw.DataFrame)
 
 
-def test_from_dict_schema(constructor: Any, request: Any) -> None:
+def test_from_dict_schema(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
     if "dask" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     schema = {"c": nw.Int16(), "d": nw.Float32()}
@@ -31,19 +32,23 @@ def test_from_dict_schema(constructor: Any, request: Any) -> None:
     assert result.collect_schema() == schema
 
 
-def test_from_dict_without_namespace(constructor: Any) -> None:
+def test_from_dict_without_namespace(constructor: Constructor) -> None:
     df = nw.from_native(constructor({"a": [1, 2, 3], "b": [4, 5, 6]})).lazy().collect()
     result = nw.from_dict({"c": df["a"], "d": df["b"]})
     compare_dicts(result, {"c": [1, 2, 3], "d": [4, 5, 6]})
 
 
-def test_from_dict_without_namespace_invalid(constructor: Any) -> None:
+def test_from_dict_without_namespace_invalid(
+    constructor: Constructor,
+) -> None:
     df = nw.from_native(constructor({"a": [1, 2, 3], "b": [4, 5, 6]})).lazy().collect()
     with pytest.raises(TypeError, match="namespace"):
         nw.from_dict({"c": nw.to_native(df["a"]), "d": nw.to_native(df["b"])})
 
 
-def test_from_dict_one_native_one_narwhals(constructor: Any) -> None:
+def test_from_dict_one_native_one_narwhals(
+    constructor: Constructor,
+) -> None:
     df = nw.from_native(constructor({"a": [1, 2, 3], "b": [4, 5, 6]})).lazy().collect()
     result = nw.from_dict({"c": nw.to_native(df["a"]), "d": df["b"]})
     expected = {"c": [1, 2, 3], "d": [4, 5, 6]}
diff --git a/tests/selectors_test.py b/tests/selectors_test.py
index ababee4a..c78a9eac 100644
--- a/tests/selectors_test.py
+++ b/tests/selectors_test.py
@@ -1,7 +1,5 @@
 from __future__ import annotations
 
-from typing import Any
-
 import pandas as pd
 import pyarrow as pa
 import pytest
@@ -14,6 +12,7 @@ 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 Constructor
 from tests.utils import compare_dicts
 
 data = {
@@ -24,28 +23,28 @@ data = {
 }
 
 
-def test_selectors(constructor: Any) -> None:
+def test_selectors(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     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(constructor: Any) -> None:
+def test_numeric(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(numeric() + 1)
     expected = {"a": [2, 2, 3], "c": [5.1, 6.0, 7.0]}
     compare_dicts(result, expected)
 
 
-def test_boolean(constructor: Any) -> None:
+def test_boolean(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(boolean())
     expected = {"d": [True, False, True]}
     compare_dicts(result, expected)
 
 
-def test_string(constructor: Any, request: Any) -> None:
+def test_string(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "dask" in str(constructor) and parse_version(pa.__version__) < (12,):
         # Dask doesn't infer `'b'` as String for old PyArrow versions
         request.applymarker(pytest.mark.xfail)
@@ -55,7 +54,7 @@ def test_string(constructor: Any, request: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_categorical(request: Any, constructor: Any) -> None:
+def test_categorical(request: pytest.FixtureRequest, constructor: Constructor) -> None:
     if "pyarrow_table_constructor" in str(constructor) and parse_version(
         pa.__version__
     ) <= (15,):  # pragma: no cover
@@ -82,7 +81,7 @@ def test_categorical(request: Any, constructor: Any) -> None:
     ],
 )
 def test_set_ops(
-    constructor: Any, selector: nw.selectors.Selector, expected: list[str]
+    constructor: Constructor, selector: nw.selectors.Selector, expected: list[str]
 ) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(selector).collect_schema().names()
@@ -90,7 +89,7 @@ def test_set_ops(
 
 
 @pytest.mark.parametrize("invalid_constructor", [pd.DataFrame, pa.table])
-def test_set_ops_invalid(invalid_constructor: Any) -> None:
+def test_set_ops_invalid(invalid_constructor: Constructor) -> None:
     df = nw.from_native(invalid_constructor(data))
     with pytest.raises(NotImplementedError):
         df.select(1 - numeric())
diff --git a/tests/series_only/array_dunder_test.py b/tests/series_only/array_dunder_test.py
index 0449199e..c09bea9e 100644
--- a/tests/series_only/array_dunder_test.py
+++ b/tests/series_only/array_dunder_test.py
@@ -10,7 +10,7 @@ from narwhals.utils import parse_version
 from tests.utils import compare_dicts
 
 
-def test_array_dunder(request: Any, constructor_eager: Any) -> None:
+def test_array_dunder(request: pytest.FixtureRequest, constructor_eager: Any) -> None:
     if "pyarrow_table" in str(constructor_eager) and parse_version(
         pa.__version__
     ) < parse_version("16.0.0"):  # pragma: no cover
@@ -21,7 +21,9 @@ def test_array_dunder(request: Any, constructor_eager: Any) -> None:
     np.testing.assert_array_equal(result, np.array([1, 2, 3], dtype="int64"))
 
 
-def test_array_dunder_with_dtype(request: Any, constructor_eager: Any) -> None:
+def test_array_dunder_with_dtype(
+    request: pytest.FixtureRequest, constructor_eager: Any
+) -> None:
     if "pyarrow_table" in str(constructor_eager) and parse_version(
         pa.__version__
     ) < parse_version("16.0.0"):  # pragma: no cover
@@ -32,7 +34,9 @@ def test_array_dunder_with_dtype(request: Any, constructor_eager: Any) -> None:
     np.testing.assert_array_equal(result, np.array([1, 2, 3], dtype=object))
 
 
-def test_array_dunder_with_copy(request: Any, constructor_eager: Any) -> None:
+def test_array_dunder_with_copy(
+    request: pytest.FixtureRequest, constructor_eager: Any
+) -> None:
     if "pyarrow_table" in str(constructor_eager) and parse_version(
         pa.__version__
     ) < parse_version("16.0.0"):  # pragma: no cover
diff --git a/tests/series_only/scatter_test.py b/tests/series_only/scatter_test.py
new file mode 100644
index 00000000..2edab2b8
--- /dev/null
+++ b/tests/series_only/scatter_test.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+import narwhals as nw
+from tests.utils import compare_dicts
+
+
+def test_scatter(constructor_eager: Any, request: pytest.FixtureRequest) -> None:
+    if "modin" in str(constructor_eager):
+        # https://github.com/modin-project/modin/issues/7392
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(
+        constructor_eager({"a": [1, 2, 3], "b": [142, 124, 132]}), eager_only=True
+    )
+    result = df.with_columns(
+        df["a"].scatter([0, 1], [999, 888]),
+        df["b"].scatter([0, 2, 1], df["b"]),
+    )
+    expected = {
+        "a": [999, 888, 3],
+        "b": [142, 132, 124],
+    }
+    compare_dicts(result, expected)
diff --git a/tests/series_only/slice_test.py b/tests/series_only/slice_test.py
new file mode 100644
index 00000000..48cf15bc
--- /dev/null
+++ b/tests/series_only/slice_test.py
@@ -0,0 +1,27 @@
+from typing import Any
+
+import narwhals.stable.v1 as nw
+from tests.utils import compare_dicts
+
+
+def test_slice(constructor_eager: Any) -> None:
+    data = {"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9], "d": [1, 4, 2]}
+    df = nw.from_native(constructor_eager(data), eager_only=True)
+    result = {"a": df["a"][[0, 1]]}
+    expected = {"a": [1, 2]}
+    compare_dicts(result, expected)
+    result = {"a": df["a"][1:]}
+    expected = {"a": [2, 3]}
+    compare_dicts(result, expected)
+    result = {"b": df[:, 1]}
+    expected = {"b": [4, 5, 6]}
+    compare_dicts(result, expected)
+    result = {"b": df[:, "b"]}
+    expected = {"b": [4, 5, 6]}
+    compare_dicts(result, expected)
+    result = {"b": df[:2, "b"]}
+    expected = {"b": [4, 5]}
+    compare_dicts(result, expected)
+    result = {"b": df[:2, 1]}
+    expected = {"b": [4, 5]}
+    compare_dicts(result, expected)
diff --git a/tests/series_only/to_arrow_test.py b/tests/series_only/to_arrow_test.py
index ebd90b7c..5181a678 100644
--- a/tests/series_only/to_arrow_test.py
+++ b/tests/series_only/to_arrow_test.py
@@ -19,7 +19,9 @@ def test_to_arrow(constructor_eager: Any) -> None:
     assert pc.all(pc.equal(result, pa.array(data, type=pa.int64())))
 
 
-def test_to_arrow_with_nulls(constructor_eager: Any, request: Any) -> None:
+def test_to_arrow_with_nulls(
+    constructor_eager: Any, request: pytest.FixtureRequest
+) -> None:
     if "pandas_constructor" in str(constructor_eager) or "modin_constructor" in str(
         constructor_eager
     ):
diff --git a/tests/series_only/to_dummy_test.py b/tests/series_only/to_dummy_test.py
index 2cf7f59c..c3d57b9a 100644
--- a/tests/series_only/to_dummy_test.py
+++ b/tests/series_only/to_dummy_test.py
@@ -18,7 +18,11 @@ def test_to_dummies(constructor_eager: Any, sep: str) -> None:
 
 
 @pytest.mark.parametrize("sep", ["_", "-"])
-def test_to_dummies_drop_first(constructor_eager: Any, sep: str) -> None:
+def test_to_dummies_drop_first(
+    request: pytest.FixtureRequest, constructor_eager: Any, sep: str
+) -> None:
+    if "cudf" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
     s = nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"].alias("a")
     result = s.to_dummies(drop_first=True, separator=sep)
     expected = {f"a{sep}2": [0, 1, 0], f"a{sep}3": [0, 0, 1]}
diff --git a/tests/series_only/to_list_test.py b/tests/series_only/to_list_test.py
index 10e41591..11d02d0d 100644
--- a/tests/series_only/to_list_test.py
+++ b/tests/series_only/to_list_test.py
@@ -8,7 +8,7 @@ from tests.utils import compare_dicts
 data = [1, 2, 3]
 
 
-def test_to_list(constructor_eager: Any, request: Any) -> None:
+def test_to_list(constructor_eager: Any, request: pytest.FixtureRequest) -> None:
     if "cudf" in str(constructor_eager):  # pragma: no cover
         request.applymarker(pytest.mark.xfail)
     s = nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"]
diff --git a/tests/series_only/to_numpy_test.py b/tests/series_only/to_numpy_test.py
index f5ed59fe..433ede16 100644
--- a/tests/series_only/to_numpy_test.py
+++ b/tests/series_only/to_numpy_test.py
@@ -9,7 +9,7 @@ from numpy.testing import assert_array_equal
 import narwhals.stable.v1 as nw
 
 
-def test_to_numpy(constructor_eager: Any, request: Any) -> None:
+def test_to_numpy(constructor_eager: Any, request: pytest.FixtureRequest) -> None:
     if "pandas_constructor" in str(constructor_eager) or "modin_constructor" in str(
         constructor_eager
     ):
diff --git a/tests/series_only/to_pandas_test.py b/tests/series_only/to_pandas_test.py
index 747bed8b..30c7906c 100644
--- a/tests/series_only/to_pandas_test.py
+++ b/tests/series_only/to_pandas_test.py
@@ -15,7 +15,7 @@ data = [1, 3, 2]
 @pytest.mark.skipif(
     parse_version(pd.__version__) < parse_version("2.0.0"), reason="too old for pyarrow"
 )
-def test_convert(request: Any, constructor_eager: Any) -> None:
+def test_convert(request: pytest.FixtureRequest, constructor_eager: Any) -> None:
     if any(
         cname in str(constructor_eager)
         for cname in ("pandas_nullable", "pandas_pyarrow", "modin")
diff --git a/tests/series_only/value_counts_test.py b/tests/series_only/value_counts_test.py
index d3d48066..d19a1440 100644
--- a/tests/series_only/value_counts_test.py
+++ b/tests/series_only/value_counts_test.py
@@ -15,7 +15,10 @@ data = [4, 4, 4, 1, 6, 6, 4, 4, 1, 1]
 @pytest.mark.parametrize("normalize", [True, False])
 @pytest.mark.parametrize("name", [None, "count_name"])
 def test_value_counts(
-    request: Any, constructor_eager: Any, normalize: Any, name: str | None
+    request: pytest.FixtureRequest,
+    constructor_eager: Any,
+    normalize: Any,
+    name: str | None,
 ) -> None:
     if "pandas_nullable_constructor" in str(constructor_eager) and parse_version(
         pd.__version__
diff --git a/tests/stable_api_test.py b/tests/stable_api_test.py
index 211ba865..375af4f4 100644
--- a/tests/stable_api_test.py
+++ b/tests/stable_api_test.py
@@ -1,14 +1,13 @@
-from typing import Any
-
 import polars as pl
 import pytest
 
 import narwhals as nw
 import narwhals.stable.v1 as nw_v1
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 
-def test_renamed_taxicab_norm(constructor: Any) -> None:
+def test_renamed_taxicab_norm(constructor: Constructor) -> None:
     # Suppose we need to rename `_l1_norm` to `_taxicab_norm`.
     # We need `narwhals.stable.v1` to stay stable. So, we
     # make the change in `narwhals`, and then add the new method
diff --git a/tests/test_group_by.py b/tests/test_group_by.py
index 4bd3427a..fa9c05f4 100644
--- a/tests/test_group_by.py
+++ b/tests/test_group_by.py
@@ -10,6 +10,7 @@ import pytest
 
 import narwhals.stable.v1 as nw
 from narwhals.utils import parse_version
+from tests.utils import Constructor
 from tests.utils import compare_dicts
 
 data = {"a": [1, 1, 3], "b": [4, 4, 6], "c": [7.0, 8, 9]}
@@ -94,7 +95,7 @@ def test_group_by_iter(constructor_eager: Any) -> None:
     assert sorted(keys) == sorted(expected_keys)
 
 
-def test_group_by_len(constructor: Any) -> None:
+def test_group_by_len(constructor: Constructor) -> None:
     result = (
         nw.from_native(constructor(data)).group_by("a").agg(nw.col("b").len()).sort("a")
     )
@@ -102,7 +103,7 @@ def test_group_by_len(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_group_by_n_unique(constructor: Any) -> None:
+def test_group_by_n_unique(constructor: Constructor) -> None:
     result = (
         nw.from_native(constructor(data))
         .group_by("a")
@@ -113,7 +114,7 @@ def test_group_by_n_unique(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_group_by_std(constructor: Any) -> None:
+def test_group_by_std(constructor: Constructor) -> None:
     data = {"a": [1, 1, 2, 2], "b": [5, 4, 3, 2]}
     result = (
         nw.from_native(constructor(data)).group_by("a").agg(nw.col("b").std()).sort("a")
@@ -122,7 +123,7 @@ def test_group_by_std(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_group_by_n_unique_w_missing(constructor: Any) -> None:
+def test_group_by_n_unique_w_missing(constructor: Constructor) -> None:
     data = {"a": [1, 1, 2], "b": [4, None, 5], "c": [None, None, 7], "d": [1, 1, 3]}
     result = (
         nw.from_native(constructor(data))
@@ -162,7 +163,7 @@ def test_group_by_empty_result_pandas() -> None:
         )
 
 
-def test_group_by_simple_named(constructor: Any) -> None:
+def test_group_by_simple_named(constructor: Constructor) -> None:
     data = {"a": [1, 1, 2], "b": [4, 5, 6], "c": [7, 2, 1]}
     df = nw.from_native(constructor(data)).lazy()
     result = (
@@ -182,7 +183,7 @@ def test_group_by_simple_named(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_group_by_simple_unnamed(constructor: Any) -> None:
+def test_group_by_simple_unnamed(constructor: Constructor) -> None:
     data = {"a": [1, 1, 2], "b": [4, 5, 6], "c": [7, 2, 1]}
     df = nw.from_native(constructor(data)).lazy()
     result = (
@@ -202,7 +203,7 @@ def test_group_by_simple_unnamed(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_group_by_multiple_keys(constructor: Any) -> None:
+def test_group_by_multiple_keys(constructor: Constructor) -> None:
     data = {"a": [1, 1, 2], "b": [4, 4, 6], "c": [7, 2, 1]}
     df = nw.from_native(constructor(data)).lazy()
     result = (
@@ -223,7 +224,7 @@ def test_group_by_multiple_keys(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_key_with_nulls(constructor: Any, request: Any) -> None:
+def test_key_with_nulls(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     if "modin" in str(constructor):
         # TODO(unassigned): Modin flaky here?
         request.applymarker(pytest.mark.skip)
@@ -246,3 +247,10 @@ def test_key_with_nulls(constructor: Any, request: Any) -> None:
         )
         expected = {"b": [4.0, 5, float("nan")], "len": [1, 1, 1], "a": [1, 2, 3]}
         compare_dicts(result, expected)
+
+
+def test_no_agg(constructor: Constructor) -> None:
+    result = nw.from_native(constructor(data)).group_by(["a", "b"]).agg().sort("a", "b")
+
+    expected = {"a": [1, 3], "b": [4, 6]}
+    compare_dicts(result, expected)
diff --git a/tests/tpch_q1_test.py b/tests/tpch_q1_test.py
index 999f32f7..c506ee0d 100644
--- a/tests/tpch_q1_test.py
+++ b/tests/tpch_q1_test.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import os
 from datetime import datetime
-from typing import Any
 from unittest import mock
 
 import pandas as pd
@@ -20,7 +19,7 @@ from tests.utils import compare_dicts
     ["pandas", "polars", "pyarrow", "dask"],
 )
 @pytest.mark.filterwarnings("ignore:.*Passing a BlockManager.*:DeprecationWarning")
-def test_q1(library: str, request: Any) -> None:
+def test_q1(library: str, request: pytest.FixtureRequest) -> None:
     if library == "pandas" and parse_version(pd.__version__) < (1, 5):
         request.applymarker(pytest.mark.xfail)
     elif library == "pandas":
@@ -99,7 +98,7 @@ def test_q1(library: str, request: Any) -> None:
     "ignore:.*Passing a BlockManager.*:DeprecationWarning",
     "ignore:.*Complex.*:UserWarning",
 )
-def test_q1_w_generic_funcs(library: str, request: Any) -> None:
+def test_q1_w_generic_funcs(library: str, request: pytest.FixtureRequest) -> None:
     if library == "pandas" and parse_version(pd.__version__) < (1, 5):
         request.applymarker(pytest.mark.xfail)
     elif library == "pandas":
diff --git a/tests/utils.py b/tests/utils.py
index 6ab703c3..b13bec19 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -4,11 +4,21 @@ import math
 import sys
 import warnings
 from typing import Any
+from typing import Callable
 from typing import Iterator
 from typing import Sequence
 
 import pandas as pd
 
+from narwhals.typing import IntoFrame
+
+if sys.version_info >= (3, 10):
+    from typing import TypeAlias  # pragma: no cover
+else:
+    from typing_extensions import TypeAlias  # pragma: no cover
+
+Constructor: TypeAlias = Callable[[Any], IntoFrame]
+
 
 def zip_strict(left: Sequence[Any], right: Sequence[Any]) -> Iterator[Any]:
     if len(left) != len(right):