Repository: narwhals-dev/narwhals
Pull request #2031 (2025-02-17 12:48:08)

fix: `is_in` was raising when used with empty list
<!--
# 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
- [ ] 🐳 Other

## Related issues

- Related issue #\<issue number\>
- Closes #\<issue number\>

## Checklist

- [ ] Code follows style guide (ruff)
- [ ] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below



Patch:
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index f2790a11..4a062721 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -545,10 +545,8 @@ class ArrowSeries(CompliantSeries):
             return self._from_native_series(ser.slice(abs(n)))
 
     def is_in(self: Self, other: Any) -> Self:
-        if isinstance(other, list) and isinstance(other[0], self.__class__):
-            # We can't use `broadcast_and_align` because we don't want to align here.
-            # `other` is just a sequence that all rows from `self` are checked against.
-            value_set = other[0]._native_series
+        if isinstance(other, pa.ChunkedArray):
+            value_set: ArrowChunkedArray | ArrowArray = other
         else:
             value_set = pa.array(other)
         ser = self._native_series
diff --git a/narwhals/_duckdb/expr.py b/narwhals/_duckdb/expr.py
index 39c16c04..0196abb4 100644
--- a/narwhals/_duckdb/expr.py
+++ b/narwhals/_duckdb/expr.py
@@ -494,7 +494,9 @@ class DuckDBExpr(CompliantExpr["duckdb.Expression"]):  # type: ignore[type-var]
 
     def is_in(self: Self, other: Sequence[Any]) -> Self:
         return self._from_call(
-            lambda _input: _input.isin(*[lit(x) for x in other]),
+            lambda _input: lit(False)  # noqa: FBT003
+            if not other
+            else _input.isin(*[lit(x) for x in other]),
             "is_in",
             expr_kind=self._expr_kind,
         )
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index cbbc1c38..169fa9d0 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -300,10 +300,6 @@ class PandasLikeSeries(CompliantSeries):
 
     def is_in(self: Self, other: Any) -> PandasLikeSeries:
         ser = self._native_series
-        if isinstance(other, list) and isinstance(other[0], self.__class__):
-            # We can't use `broadcast_and_align` because we don't want to align here.
-            # `other` is just a sequence that all rows from `self` are checked against.
-            other = other[0]._native_series
         res = ser.isin(other)
         return self._from_native_series(res)
 
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 8819d986..d2642c86 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -20,6 +20,7 @@ from narwhals.expr_dt import ExprDateTimeNamespace
 from narwhals.expr_list import ExprListNamespace
 from narwhals.expr_name import ExprNameNamespace
 from narwhals.expr_str import ExprStringNamespace
+from narwhals.translate import to_native
 from narwhals.utils import _validate_rolling_arguments
 from narwhals.utils import flatten
 from narwhals.utils import issue_deprecation_warning
@@ -1254,7 +1255,7 @@ class Expr:
         if isinstance(other, Iterable) and not isinstance(other, (str, bytes)):
             return self.__class__(
                 lambda plx: self._to_compliant_expr(plx).is_in(
-                    extract_compliant(plx, other, strings_are_column_names=False)
+                    to_native(other, pass_through=True)
                 ),
                 self._metadata,
             )
diff --git a/narwhals/series.py b/narwhals/series.py
index 409f518c..829ea3d3 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -805,7 +805,7 @@ class Series(Generic[IntoSeriesT]):
             ]
         """
         return self._from_compliant_series(
-            self._compliant_series.is_in(self._extract_native(other))
+            self._compliant_series.is_in(to_native(other, pass_through=True))
         )
 
     def arg_true(self: Self) -> Self:


Test patch:
diff --git a/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
index 0c91c64b..d1c4dcc7 100644
--- a/.github/workflows/downstream_tests.yml
+++ b/.github/workflows/downstream_tests.yml
@@ -83,6 +83,8 @@ jobs:
         run: |
           cd marimo
           . .venv/bin/activate
+          # temporary to get CI green
+          uv pip install "panel<1.6.1"
           uv pip install -e ".[dev]"
           which python
       - name: install-narwhals-dev
diff --git a/tests/expr_and_series/is_in_test.py b/tests/expr_and_series/is_in_test.py
index a2f1592b..0c658874 100644
--- a/tests/expr_and_series/is_in_test.py
+++ b/tests/expr_and_series/is_in_test.py
@@ -18,6 +18,14 @@ def test_expr_is_in(constructor: Constructor) -> None:
     assert_equal_data(result, expected)
 
 
+def test_expr_is_in_empty_list(constructor: Constructor) -> None:
+    df = nw.from_native(constructor(data))
+    result = df.select(nw.col("a").is_in([]))
+    expected = {"a": [False, False, False, False]}
+
+    assert_equal_data(result, expected)
+
+
 def test_ser_is_in(constructor_eager: ConstructorEager) -> None:
     ser = nw.from_native(constructor_eager(data), eager_only=True)["a"]
     result = {"a": ser.is_in([4, 5])}