Repository: narwhals-dev/narwhals
Pull request #2504 (2025-05-06 19:32:31)

chore: Ensure (`join|join_asof`) predicate lengths match
<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [x] 🐳 Other

## Related issues

- https://github.com/narwhals-dev/narwhals/pull/2000#discussion_r2076086401

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below



Patch:
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 1364049a..c6ae8ad4 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -273,6 +273,12 @@ class BaseFrame(Generic[_FrameT]):
         if on is not None:
             left_on = right_on = on
 
+        if (isinstance(left_on, list) and isinstance(right_on, list)) and (
+            len(left_on) != len(right_on)
+        ):
+            msg = "`left_on` and `right_on` must have the same length."
+            raise ValueError(msg)
+
         return self._with_compliant(
             self._compliant_frame.join(
                 self._extract_compliant(other),
@@ -288,7 +294,7 @@ class BaseFrame(Generic[_FrameT]):
             self._compliant_frame.gather_every(n=n, offset=offset)
         )
 
-    def join_asof(
+    def join_asof(  # noqa: C901
         self,
         other: Self,
         *,
@@ -332,6 +338,13 @@ class BaseFrame(Generic[_FrameT]):
             by_left = [by_left]
         if isinstance(by_right, str):
             by_right = [by_right]
+
+        if (isinstance(by_left, list) and isinstance(by_right, list)) and (
+            len(by_left) != len(by_right)
+        ):
+            msg = "`by_left` and `by_right` must have the same length."
+            raise ValueError(msg)
+
         return self._with_compliant(
             self._compliant_frame.join_asof(
                 self._extract_compliant(other),


Test patch:
diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py
index 5e60ab93..761876db 100644
--- a/tests/frame/join_test.py
+++ b/tests/frame/join_test.py
@@ -462,6 +462,12 @@ def test_join_keys_exceptions(constructor: Constructor, how: str) -> None:
     ):
         df.join(df, how=how, on="antananarivo", right_on="antananarivo")  # type: ignore[arg-type]
 
+    with pytest.raises(
+        ValueError,
+        match="`left_on` and `right_on` must have the same length.",
+    ):
+        df.join(df, how=how, left_on=["antananarivo", "bob"], right_on="antananarivo")  # type: ignore[arg-type]
+
 
 @pytest.mark.parametrize(
     ("strategy", "expected"),
@@ -772,6 +778,17 @@ def test_joinasof_by_exceptions(constructor: Constructor) -> None:
     ):
         df.join_asof(df, on="antananarivo", by_right="bob", by="bob")
 
+    with pytest.raises(
+        ValueError,
+        match="`by_left` and `by_right` must have the same length.",
+    ):
+        df.join_asof(
+            df,
+            on="antananarivo",
+            by_left=["antananarivo", "bob"],
+            by_right=["antananarivo"],
+        )
+
 
 def test_join_duplicate_column_names(
     constructor: Constructor, request: pytest.FixtureRequest