Skip to content

feat: support list.{len,get,min,max,mean,median,sum,sort} for Dask - #4017

Open
jonasdedden wants to merge 2 commits into
narwhals-dev:mainfrom
jonasdedden:upstream/dask-list-methods
Open

jonasdedden wants to merge 2 commits into
narwhals-dev:mainfrom
jonasdedden:upstream/dask-list-methods

Conversation

@jonasdedden

@jonasdedden jonasdedden commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds list.len, list.get, list.min, list.max, list.mean, list.median, list.sum and list.sort for Dask. Only list.unique remains unimplemented, as it is for pandas and PyArrow.
Each partition is wrapped in a PandasLikeSeries and the call isforwarded to the existing pandas .list method, so Dask reuses the pandas implementation (and its pyarrow helpers) instead of adding new list logic. list.contains now goes through the same path.

meta is computed by running the same method on the empty _meta instead of being inferred by Dask, because inference would wrap errors raised on user input (e.g. the InvalidOperationError for a mismatched list.contains item from #3915) in a ValueError.

As with pandas, only pyarrow-backed lists are supported, and list.median is pyarrow's approximate median. The tests skip Dask on pandas<2.2, as they already do for pandas, since casting to List needs it.

What type of PR is this? (check all applicable)

  • 💾 Refactor
  • ✨ Feature
  • 🐛 Bug Fix
  • 🔧 Optimization
  • 📝 Documentation
  • ✅ Test
  • 🐳 Other

Related issues

AI assistance

  • No AI tools were used for this PR.
  • AI tools were used.

Checklist

  • Code follows style guide (ruff)

  • Tests added

  • Documented the changes (N/A, the API docs already list these methods)

  • If this is your first PR to narwhals, attach a screenshot of pytest passing locally (not CI):

    PYTEST_ADDOPTS="--numprocesses=logical" \
    make run-ci DEPS="--extra pandas --extra dask --group core-tests --group sklearn --group plugins" \
    CMD="pytest tests --cov=src --cov=tests --runslow --constructors=pandas,pandas[nullable],pandas[pyarrow],pyarrow,polars[eager],polars[lazy],dask,duckdb,sqlframe"

Summary by CodeRabbit

  • New Features
    • Dask now supports list length, item retrieval, minimum, maximum, mean, median, sum, and sorting operations.
    • List containment is supported through the same operations path.
  • Bug Fixes
    • List item retrieval now preserves the source index on pandas versions earlier than 3.0.

@jonasdedden

Copy link
Copy Markdown
Contributor Author

@FBruzzesi this really was minimal work required to get almost all of the List namespace working for Dask 😁

@FBruzzesi FBruzzesi added enhancement New feature or request dask Issue is related to dask backend labels Oct 3, 2026

@FBruzzesi FBruzzesi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonasdedden - I agree that the implementation is quite straightforward and low effort to maintain.

After the suggested change in src/narwhals/_dask/expr_list.py I asked Claude for a review and it spotted an issue for the case in which there is a non-null last row and pandas<3.0.0 as pandas' own list[i] accessor returns a fresh 0-based index on 2.2.3 and 2.3.3 (fixed in 3.0). Dask lines partitions up by index, so every partition after the first came back as nulls, and there is nothing in CI to catch that.

The suggested fixes are:

diff --git a/src/narwhals/_pandas_like/series_list.py b/src/narwhals/_pandas_like/series_list.py
index 8f69ac08f..3b0843bfe 100644
--- a/src/narwhals/_pandas_like/series_list.py
+++ b/src/narwhals/_pandas_like/series_list.py
@@ -48,6 +48,10 @@ class PandasLikeSeriesListNamespace(
 
     def get(self, index: int) -> PandasLikeSeries:
         result = self.native.list[index]
+        implementation, backend_version = self.implementation, self.backend_version
+        if implementation.is_pandas() and backend_version < (3, 0):  # pragma: no cover
+            # `result` is a new object so it's safe to do this inplace.
+            result.index = self.native.index
         result.name = self.native.name
         return self.with_native(result)

and a test to check it:

diff --git a/tests/expr_and_series/list/get_test.py b/tests/expr_and_series/list/get_test.py
index 8a3e4ea41..be8cc607e 100644
--- a/tests/expr_and_series/list/get_test.py
+++ b/tests/expr_and_series/list/get_test.py
@@ -8,10 +8,10 @@ import pytest
 import narwhals as nw
 from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data
 
-data = {"a": [[1, 2], [None, 3], [None], None]}
+data = {"a": [[1, 2], [None, 3], [None], None, [4]]}
 
 
-@pytest.mark.parametrize(("index", "expected"), [(0, {"a": [1, None, None, None]})])
+@pytest.mark.parametrize(("index", "expected"), [(0, {"a": [1, None, None, None, 4]})])
 def test_get_expr(
     request: pytest.FixtureRequest, constructor: Constructor, index: int, expected: Any
 ) -> None:
@@ -30,7 +30,7 @@ def test_get_expr(
     assert_equal_data(result, expected)
 
 
-@pytest.mark.parametrize(("index", "expected"), [(0, {"a": [1, None, None, None]})])
+@pytest.mark.parametrize(("index", "expected"), [(0, {"a": [1, None, None, None, 4]})])
 def test_get_series(
     request: pytest.FixtureRequest,
     constructor_eager: ConstructorEager,

Comment thread src/narwhals/_dask/expr_list.py
…st partitions

Address review on narwhals-dev#4017:
- pandas<3 `list[i]` returns a fresh 0-based index, which made every Dask
  partition after the first come back as nulls; restore the original index.
- Extend `list.get` test data with a non-null last row to cover this.
- Replace `*args/**kwargs` + `getattr` dispatch in the Dask list namespace
  with callables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGTmcygmaAm4q8hXVvjH4C
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
Generated by CodeRabbit — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b1bdc4a8-12b4-488a-95b2-27aeb2904882
📥 Commits

Reviewing files that changed from the base of the PR and between c7f93c9 and 514199f.

📒 Files selected for processing (10)
  • src/narwhals/_dask/expr_list.py
  • src/narwhals/_pandas_like/series_list.py
  • tests/expr_and_series/list/get_test.py
  • tests/expr_and_series/list/len_test.py
  • tests/expr_and_series/list/max_test.py
  • tests/expr_and_series/list/mean_test.py
  • tests/expr_and_series/list/median_test.py
  • tests/expr_and_series/list/min_test.py
  • tests/expr_and_series/list/sort_test.py
  • tests/expr_and_series/list/sum_test.py

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Dask list expressions now support len, contains, get, min, max, mean, median, sum, and sort through partition-based pandas-like operations. The changes also adjust pandas get index handling and update list-operation tests for Dask.

Changes

Dask list operations

Layer / File(s) Summary
Partition-based list operations
src/narwhals/_dask/expr_list.py
A shared mapper applies pandas-like list operations to Dask partitions, rejects non-PyArrow-backed list dtypes, and supplies explicit metadata.
Get compatibility and backend tests
src/narwhals/_pandas_like/series_list.py, tests/expr_and_series/list/*_test.py
For pandas versions before 3.0, get restores the source index. Tests update backend conditions and expected results for list operations.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant DaskExprListNamespace
  participant Dask map_partitions
  participant PandasLikeSeries
  DaskExprListNamespace->>Dask map_partitions: Apply the shared partition adapter with explicit metadata
  Dask map_partitions->>PandasLikeSeries: Wrap each pandas partition and run the list operation
  PandasLikeSeries-->>Dask map_partitions: Return the native pandas series
Loading

Suggested reviewers: fbruzzesi

Merge Risk: ⚪ Minimal · up to 51419

This change adds Dask support for several list operations and fixes index handling for list get on older pandas versions. No merge-blocking risk was identified.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 51419

The new operations reuse existing list computations within Dask’s established partition-execution path. The reviewed changes do not introduce a material security risk or grant new privileges.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The shared mapper propagates behavior across nine list methods and the partitions of each selected series. This fanout represents a shared library execution contract, not nine independently exposed services.

Trust Boundaries and Controls

  • inferred — The inspected source-to-computation path does not add an authentication, credential, filesystem, or network sink. It extends computations within the existing Dask execution boundary. Deployment-specific worker privileges and tenant isolation were not established by the available evidence.

Resilience and Maintainability Implications

  • inferred — The inspected adapter does not introduce a shared-state transaction or persistent lifecycle. Metadata computation precedes graph mapping, and index repair applies to an extracted result. Failures do not require a new application-level reservation, rollback, or cleanup protocol in this path.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding the listed Dask list operations.
Description check ✅ Passed The description explains the supported operations, implementation approach, metadata handling, limitations, related issues, AI use, and testing. It also completes the applicable template sections and …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🧪 Generate unit tests (beta)
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jonasdedden

Copy link
Copy Markdown
Contributor Author

@FBruzzesi thanks so much! just FYI, sadly I'm not at a computer right now, I let Claude do things, the CI failure seems to be not because of this PR: (Claude output raw pasted)

The pointblank failure is not caused by this PR: the same tests fail identically on narwhals main (1acc082, v2.27.0). It comes from a change already on main. I haven't changed or pushed anything.

  1. tests/test_datascan.py::test_datascan_json_output is a real regression, introduced by fix: preserve nulls when casting to String on pandas pre v3.0.0 #3939 ("preserve nulls when casting to String on pandas pre v3.0.0", commit 9c973b9). I found it by bisecting between 2.26.0 and main.

    • pointblank's string profile runs cast(nw.String).str.len_chars() and then .median().
    • Before fix: preserve nulls when casting to String on pandas pre v3.0.0 #3939, an all-None column became the literal string "None" on pandas before 3.0. So len_chars returned Int64 4s and median worked, by accident.
    • Now the nulls are kept. But pandas 2.x's .str.len() on an all-null object column returns object ([None, None, …]), not a number. narwhals then reports the result as String, and median raises InvalidOperationError: median operation not supported for non-numeric input type.
    • The real bug is in PandasLikeSeriesStringNamespace.len_chars (src/narwhals/_pandas_like/series_str.py:38): it should always return a numeric dtype. With at least one non-null value, pandas gives float64; only the all-null case falls through to object.
    • It's a Hypothesis test, so it only fails when an all-None column is generated or replayed. That's why it's intermittent.
  2. tests/test_validate.py::test_write_file_permission_error fails here only because this container runs as root, so permission errors never trigger. It likely isn't what failed in CI, but I can't confirm that without the log.

fix: make the pandas-like len_chars cast an all-null object result to a numeric dtype, with a test like nw.col("a").cast(nw.String).str.len_chars() on an all-None pandas column. Once that's on main, merging main into this branch would clear the job. I can draft it on a new branch if you want.

@FBruzzesi

Copy link
Copy Markdown
Member

Thanks for investigating this. It seems that we are not testing pointblank with the HEAD branch but only with the latest release.... that's really annoying. I will try to fix that today or tomorrow 🫥

@jonasdedden

Copy link
Copy Markdown
Contributor Author

Again, feel free to work on this PR if you want 🫶 Could potentially be that I won't have much capacity this week

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dask Issue is related to dask backend enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants