Repository: narwhals-dev/narwhals
Pull request #580 (2024-07-22 21:24:47)

feat: add `nulls_last` argument in `Series/Expr.sort` 
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- [Related ~issue~ comment](https://github.com/narwhals-dev/narwhals/pull/577#discussion_r1686927536)

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added 
- [x] Documented the changes




Patch:
diff --git a/docs/installation.md b/docs/installation.md
index 6131e00f..0241d873 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.1.4'
+'1.1.5'
 ```
 then installation worked correctly!
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index b9175d19..1c75122b 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -46,7 +46,7 @@ from narwhals.utils import maybe_align_index
 from narwhals.utils import maybe_convert_dtypes
 from narwhals.utils import maybe_set_index
 
-__version__ = "1.1.4"
+__version__ = "1.1.5"
 
 __all__ = [
     "selectors",
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index a04c70bb..7c92fd4b 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -251,8 +251,10 @@ class ArrowExpr:
     def unique(self: Self) -> Self:
         return reuse_series_implementation(self, "unique")
 
-    def sort(self: Self, *, descending: bool = False) -> Self:
-        return reuse_series_implementation(self, "sort", descending=descending)
+    def sort(self: Self, *, descending: bool = False, nulls_last: bool = False) -> Self:
+        return reuse_series_implementation(
+            self, "sort", descending=descending, nulls_last=nulls_last
+        )
 
     @property
     def dt(self: Self) -> ArrowExprDateTimeNamespace:
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 53524278..0346fabb 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -472,12 +472,15 @@ class ArrowSeries:
         pc = get_pyarrow_compute()
         return self._from_native_series(pc.unique(self._native_series))
 
-    def sort(self: Self, *, descending: bool = False) -> ArrowSeries:
+    def sort(
+        self: Self, *, descending: bool = False, nulls_last: bool = False
+    ) -> ArrowSeries:
         pc = get_pyarrow_compute()
         series = self._native_series
         order = "descending" if descending else "ascending"
+        null_placement = "at_end" if nulls_last else "at_start"
         sorted_indices = pc.array_sort_indices(
-            series, order=order, null_placement="at_start"
+            series, order=order, null_placement=null_placement
         )
 
         return self._from_native_series(pc.take(series, sorted_indices))
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 70670287..db40fb65 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -197,6 +197,9 @@ class PandasLikeDataFrame:
         *exprs: IntoPandasLikeExpr,
         **named_exprs: IntoPandasLikeExpr,
     ) -> Self:
+        if exprs and all(isinstance(x, str) for x in exprs) and not named_exprs:
+            # This is a simple slice => fastpath!
+            return self._from_native_dataframe(self._native_dataframe.loc[:, exprs])
         new_series = evaluate_into_exprs(self, *exprs, **named_exprs)
         if not new_series:
             # return empty dataframe, like Polars does
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index f846da61..de4540e9 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -229,8 +229,10 @@ class PandasLikeExpr:
     def drop_nulls(self) -> Self:
         return reuse_series_implementation(self, "drop_nulls")
 
-    def sort(self, *, descending: bool = False) -> Self:
-        return reuse_series_implementation(self, "sort", descending=descending)
+    def sort(self, *, descending: bool = False, nulls_last: bool = False) -> Self:
+        return reuse_series_implementation(
+            self, "sort", descending=descending, nulls_last=nulls_last
+        )
 
     def abs(self) -> Self:
         return reuse_series_implementation(self, "abs")
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 22a2d862..b86de7f7 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -28,12 +28,25 @@ class PandasLikeGroupBy:
     def __init__(self, df: PandasLikeDataFrame, keys: list[str]) -> None:
         self._df = df
         self._keys = list(keys)
-        self._grouped = self._df._native_dataframe.groupby(
-            list(self._keys),
-            sort=False,
-            as_index=True,
-            dropna=False,
-        )
+        if (
+            self._df._implementation is Implementation.PANDAS
+            and self._df._backend_version < (1, 0)
+        ):  # pragma: no cover
+            if self._df._native_dataframe.loc[:, self._keys].isna().any().any():
+                msg = "Grouping by null values is not supported in pandas < 1.0.0"
+                raise NotImplementedError(msg)
+            self._grouped = self._df._native_dataframe.groupby(
+                list(self._keys),
+                sort=False,
+                as_index=True,
+            )
+        else:
+            self._grouped = self._df._native_dataframe.groupby(
+                list(self._keys),
+                sort=False,
+                as_index=True,
+                dropna=False,
+            )
 
     def agg(
         self,
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 36d69f0d..946725eb 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -465,13 +465,12 @@ class PandasLikeSeries:
         return self._from_native_series(self._native_series.shift(n))
 
     def sort(
-        self,
-        *,
-        descending: bool | Sequence[bool] = False,
+        self, *, descending: bool = False, nulls_last: bool = False
     ) -> PandasLikeSeries:
         ser = self._native_series
+        na_position = "last" if nulls_last else "first"
         return self._from_native_series(
-            ser.sort_values(ascending=not descending, na_position="first").rename(
+            ser.sort_values(ascending=not descending, na_position=na_position).rename(
                 self.name
             )
         )
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index 11bc121e..880edbb2 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -184,6 +184,21 @@ class PolarsSeries:
 
         return PolarsDataFrame(result, backend_version=self._backend_version)
 
+    def sort(self, *, descending: bool = False, nulls_last: bool = False) -> Self:
+        if self._backend_version < (0, 20, 6):  # pragma: no cover
+            result = self._native_series.sort(descending=descending)
+
+            if nulls_last:
+                pl = get_polars()
+                is_null = result.is_null()
+                result = pl.concat([result.filter(~is_null), result.filter(is_null)])
+        else:
+            result = self._native_series.sort(
+                descending=descending, nulls_last=nulls_last
+            )
+
+        return self._from_native_series(result)
+
     @property
     def dt(self) -> PolarsSeriesDateTimeNamespace:
         return PolarsSeriesDateTimeNamespace(self)
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 5b51f5a5..d709f1f2 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -783,12 +783,13 @@ class Expr:
         """
         return self.__class__(lambda plx: self._call(plx).shift(n))
 
-    def sort(self, *, descending: bool = False) -> Self:
+    def sort(self, *, descending: bool = False, nulls_last: bool = False) -> Self:
         """
         Sort this column. Place null values first.
 
         Arguments:
             descending: Sort in descending order.
+            nulls_last: Place null values last instead of first.
 
         Examples:
             >>> import narwhals as nw
@@ -849,7 +850,9 @@ class Expr:
             │ 1    │
             └──────┘
         """
-        return self.__class__(lambda plx: self._call(plx).sort(descending=descending))
+        return self.__class__(
+            lambda plx: self._call(plx).sort(descending=descending, nulls_last=nulls_last)
+        )
 
     # --- transform ---
     def is_between(
diff --git a/narwhals/series.py b/narwhals/series.py
index 9da9a6c5..9e0e8c17 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -964,12 +964,13 @@ class Series:
         """
         return self._from_compliant_series(self._compliant_series.alias(name=name))
 
-    def sort(self, *, descending: bool = False) -> Self:
+    def sort(self, *, descending: bool = False, nulls_last: bool = False) -> Self:
         """
         Sort this Series. Place null values first.
 
         Arguments:
             descending: Sort in descending order.
+            nulls_last: Place null values last instead of first.
 
         Examples:
             >>> import pandas as pd
@@ -1023,7 +1024,7 @@ class Series:
             ]
         """
         return self._from_compliant_series(
-            self._compliant_series.sort(descending=descending)
+            self._compliant_series.sort(descending=descending, nulls_last=nulls_last)
         )
 
     def is_null(self) -> Self:
diff --git a/pyproject.toml b/pyproject.toml
index ebbea35b..e392abc1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "narwhals"
-version = "1.1.4"
+version = "1.1.5"
 authors = [
   { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
 ]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index a9d6f04d..0586d00c 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,5 +1,4 @@
 covdefaults
-ibis-framework
 pandas
 polars[timezones]
 pre-commit


Test patch:
diff --git a/tests/expr_and_series/sort_test.py b/tests/expr_and_series/sort_test.py
index 22c0be6b..b9b4773a 100644
--- a/tests/expr_and_series/sort_test.py
+++ b/tests/expr_and_series/sort_test.py
@@ -1,31 +1,46 @@
 from typing import Any
 
-import numpy as np
 import pytest
 
 import narwhals.stable.v1 as nw
-from tests.utils import compare_dicts
 
-data = {"a": [1, 3, 2], "b": [0, 2, -1]}
+data = {"a": [0, 0, 2, -1], "b": [1, 3, 2, None]}
 
 
 @pytest.mark.parametrize(
-    ("descending", "expected"),
+    ("descending", "nulls_last", "expected"),
     [
-        (True, {"a": [3, 2, 1], "b": [0, 2, -1]}),
-        (False, {"a": [1, 2, 3], "b": [0, 2, -1]}),
+        (True, True, {"a": [0, 0, 2, -1], "b": [3, 2, 1, None]}),
+        (True, False, {"a": [0, 0, 2, -1], "b": [None, 3, 2, 1]}),
+        (False, True, {"a": [0, 0, 2, -1], "b": [1, 2, 3, None]}),
+        (False, False, {"a": [0, 0, 2, -1], "b": [None, 1, 2, 3]}),
     ],
 )
-def test_sort_expr(constructor: Any, descending: Any, expected: Any) -> None:
+def test_sort_expr(
+    constructor: Any, descending: Any, nulls_last: Any, expected: Any
+) -> None:
     df = nw.from_native(constructor(data), eager_only=True)
-    result = df.select(nw.col("a").sort(descending=descending), "b")
-    compare_dicts(result, expected)
+    result = nw.to_native(
+        df.select(
+            "a",
+            nw.col("b").sort(descending=descending, nulls_last=nulls_last),
+        )
+    )
+    assert result.equals(constructor(expected))
 
 
 @pytest.mark.parametrize(
-    ("descending", "expected"), [(True, [3, 2, 1]), (False, [1, 2, 3])]
+    ("descending", "nulls_last", "expected"),
+    [
+        (True, True, [3, 2, 1, None]),
+        (True, False, [None, 3, 2, 1]),
+        (False, True, [1, 2, 3, None]),
+        (False, False, [None, 1, 2, 3]),
+    ],
 )
-def test_sort_series(constructor_series: Any, descending: Any, expected: Any) -> None:
-    series = nw.from_native(constructor_series(data["a"]), series_only=True)
-    result = series.sort(descending=descending)
-    assert (result.to_numpy() == np.array(expected)).all()
+def test_sort_series(
+    constructor_series: Any, descending: Any, nulls_last: Any, expected: Any
+) -> None:
+    series = nw.from_native(constructor_series(data["b"]), series_only=True)
+    result = series.sort(descending=descending, nulls_last=nulls_last)
+    assert result == nw.from_native(constructor_series(expected), series_only=True)
diff --git a/tests/frame/concat_test.py b/tests/frame/concat_test.py
new file mode 100644
index 00000000..e52b33e8
--- /dev/null
+++ b/tests/frame/concat_test.py
@@ -0,0 +1,50 @@
+from typing import Any
+
+import pytest
+
+import narwhals.stable.v1 as nw
+from tests.utils import compare_dicts
+
+
+def test_concat_horizontal(constructor_with_lazy: Any) -> None:
+    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
+    df_left = nw.from_native(constructor_with_lazy(data))
+
+    data_right = {"c": [6, 12, -1], "d": [0, -4, 2]}
+    df_right = nw.from_native(constructor_with_lazy(data_right))
+
+    result = nw.concat([df_left, df_right], how="horizontal")
+    expected = {
+        "a": [1, 3, 2],
+        "b": [4, 4, 6],
+        "z": [7.0, 8, 9],
+        "c": [6, 12, -1],
+        "d": [0, -4, 2],
+    }
+    compare_dicts(result, expected)
+
+    with pytest.raises(ValueError, match="No items"):
+        nw.concat([])
+
+
+def test_concat_vertical(constructor_with_lazy: Any) -> None:
+    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
+    df_left = (
+        nw.from_native(constructor_with_lazy(data))
+        .rename({"a": "c", "b": "d"})
+        .drop("z")
+        .lazy()
+    )
+
+    data_right = {"c": [6, 12, -1], "d": [0, -4, 2]}
+    df_right = nw.from_native(constructor_with_lazy(data_right)).lazy()
+
+    result = nw.concat([df_left, df_right], how="vertical")
+    expected = {"c": [1, 3, 2, 6, 12, -1], "d": [4, 4, 6, 0, -4, 2]}
+    compare_dicts(result, expected)
+
+    with pytest.raises(ValueError, match="No items"):
+        nw.concat([], how="vertical")
+
+    with pytest.raises((Exception, TypeError), match="unable to vstack"):
+        nw.concat([df_left, df_right.rename({"d": "i"})], how="vertical").collect()
diff --git a/tests/frame/drop_test.py b/tests/frame/drop_test.py
new file mode 100644
index 00000000..f7e5248f
--- /dev/null
+++ b/tests/frame/drop_test.py
@@ -0,0 +1,21 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+import narwhals.stable.v1 as nw
+
+
+@pytest.mark.parametrize(
+    ("drop", "left"),
+    [
+        (["a"], ["b", "z"]),
+        (["a", "b"], ["z"]),
+    ],
+)
+def test_drop(constructor: Any, drop: list[str], left: list[str]) -> None:
+    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
+    df = nw.from_native(constructor(data))
+    assert df.drop(drop).columns == left
+    assert df.drop(*drop).columns == left
diff --git a/tests/frame/interchange_schema_test.py b/tests/frame/interchange_schema_test.py
index 72d4158f..df901522 100644
--- a/tests/frame/interchange_schema_test.py
+++ b/tests/frame/interchange_schema_test.py
@@ -1,17 +1,11 @@
 from datetime import date
 
-import ibis
 import polars as pl
 import pytest
 
 import narwhals.stable.v1 as nw
-from narwhals.utils import parse_version
 
 
-@pytest.mark.skipif(
-    parse_version(ibis.__version__) < (6, 0),
-    reason="too old, requires interchange protocol",
-)
 def test_interchange_schema() -> None:
     df_pl = pl.DataFrame(
         {
@@ -47,8 +41,7 @@ def test_interchange_schema() -> None:
             "n": pl.Boolean,
         },
     )
-    tbl = ibis.memtable(df_pl)
-    df = nw.from_native(tbl, eager_or_interchange_only=True)
+    df = nw.from_native(df_pl.__dataframe__(), eager_or_interchange_only=True)
     result = df.schema
     expected = {
         "a": nw.Int64,
@@ -62,7 +55,7 @@ def test_interchange_schema() -> None:
         "i": nw.Float64,
         "j": nw.Float32,
         "k": nw.String,
-        "l": nw.String,  # https://github.com/ibis-project/ibis/issues/9570
+        "l": nw.Categorical,
         "m": nw.Datetime,
         "n": nw.Boolean,
     }
@@ -70,31 +63,22 @@ def test_interchange_schema() -> None:
     assert df["a"].dtype == nw.Int64
 
 
-@pytest.mark.skipif(
-    parse_version(ibis.__version__) < (6, 0),
-    reason="too old, requires interchange protocol",
-)
 def test_invalid() -> None:
-    df = pl.DataFrame({"a": [1, 2, 3]})
-    tbl = ibis.memtable(df)
+    df = pl.DataFrame({"a": [1, 2, 3]}).__dataframe__()
     with pytest.raises(
         NotImplementedError, match="is not supported for metadata-only dataframes"
     ):
-        nw.from_native(tbl, eager_or_interchange_only=True).select("a")
+        nw.from_native(df, eager_or_interchange_only=True).select("a")
     with pytest.raises(TypeError, match="Cannot only use `series_only=True`"):
-        nw.from_native(tbl, eager_only=True)
+        nw.from_native(df, eager_only=True)
     with pytest.raises(ValueError, match="Invalid parameter combination"):
-        nw.from_native(tbl, eager_only=True, eager_or_interchange_only=True)  # type: ignore[call-overload]
+        nw.from_native(df, eager_only=True, eager_or_interchange_only=True)  # type: ignore[call-overload]
 
 
-@pytest.mark.skipif(
-    parse_version(ibis.__version__) < (6, 0),
-    reason="too old, requires interchange protocol",
-)
 def test_get_level() -> None:
     df = pl.DataFrame({"a": [1, 2, 3]})
-    tbl = ibis.memtable(df)
+    assert nw.get_level(nw.from_native(df)) == "full"
     assert (
-        nw.get_level(nw.from_native(tbl, eager_or_interchange_only=True)) == "interchange"
+        nw.get_level(nw.from_native(df.__dataframe__(), eager_or_interchange_only=True))
+        == "interchange"
     )
-    assert nw.get_level(nw.from_native(df)) == "full"
diff --git a/tests/frame/item_test.py b/tests/frame/item_test.py
new file mode 100644
index 00000000..7471937e
--- /dev/null
+++ b/tests/frame/item_test.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import re
+from typing import Any
+
+import pytest
+
+import narwhals.stable.v1 as nw
+
+
+@pytest.mark.parametrize(
+    ("row", "column", "expected"),
+    [(0, 2, 7), (1, "z", 8)],
+)
+def test_item(
+    constructor: Any, row: int | None, column: int | str | None, expected: Any
+) -> None:
+    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
+    df = nw.from_native(constructor(data), eager_only=True)
+    assert df.item(row, column) == expected
+    assert df.select("a").head(1).item() == 1
+
+
+@pytest.mark.parametrize(
+    ("row", "column", "err_msg"),
+    [
+        (
+            0,
+            None,
+            re.escape("cannot call `.item()` with only one of `row` or `column`"),
+        ),
+        (
+            None,
+            0,
+            re.escape("cannot call `.item()` with only one of `row` or `column`"),
+        ),
+        (
+            None,
+            None,
+            re.escape("can only call `.item()` if the dataframe is of shape (1, 1)"),
+        ),
+    ],
+)
+def test_item_value_error(
+    constructor: Any, row: int | None, column: int | str | None, err_msg: str
+) -> None:
+    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
+    with pytest.raises(ValueError, match=err_msg):
+        nw.from_native(constructor(data), eager_only=True).item(row, column)
diff --git a/tests/frame/test_common.py b/tests/frame/test_common.py
index 9030f99c..e2fa3ff7 100644
--- a/tests/frame/test_common.py
+++ b/tests/frame/test_common.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import re
 import warnings
 from typing import Any
 
@@ -79,11 +78,9 @@ def test_columns(constructor_with_lazy: Any) -> None:
 
 
 def test_expr_binary(request: Any, constructor: Any) -> None:
-    if (
-        "pyarrow_table" in str(constructor)
-        or "pandas_pyarrow"
-        in str(constructor)  # pandas pyarrow raises NotImplementedError for __mod__
-    ):
+    if "pyarrow_table" in str(constructor) or "pandas_pyarrow" in str(
+        constructor
+    ):  # pandas pyarrow raises NotImplementedError for __mod__
         request.applymarker(pytest.mark.xfail)
     df_raw = constructor(data)
     result = nw.from_native(df_raw).with_columns(
@@ -159,56 +156,6 @@ def test_expr_na(constructor_with_lazy: Any) -> None:
     compare_dicts(result_nna, expected)
 
 
-@pytest.mark.parametrize(
-    ("drop", "left"),
-    [
-        (["a"], ["b", "z"]),
-        (["a", "b"], ["z"]),
-    ],
-)
-def test_drop(constructor: Any, drop: list[str], left: list[str]) -> None:
-    df = nw.from_native(constructor(data))
-    assert df.drop(drop).columns == left
-    assert df.drop(*drop).columns == left
-
-
-def test_concat_horizontal(constructor_with_lazy: Any) -> None:
-    df_left = nw.from_native(constructor_with_lazy(data))
-    df_right = nw.from_native(constructor_with_lazy(data_right))
-    result = nw.concat([df_left, df_right], how="horizontal")
-    expected = {
-        "a": [1, 3, 2],
-        "b": [4, 4, 6],
-        "z": [7.0, 8, 9],
-        "c": [6, 12, -1],
-        "d": [0, -4, 2],
-    }
-    compare_dicts(result, expected)
-
-    with pytest.raises(ValueError, match="No items"):
-        nw.concat([])
-
-
-def test_concat_vertical(constructor_with_lazy: Any) -> None:
-    df_left = (
-        nw.from_native(constructor_with_lazy(data))
-        .rename({"a": "c", "b": "d"})
-        .drop("z")
-        .lazy()
-    )
-    df_right = nw.from_native(constructor_with_lazy(data_right)).lazy()
-
-    result = nw.concat([df_left, df_right], how="vertical")
-    expected = {"c": [1, 3, 2, 6, 12, -1], "d": [4, 4, 6, 0, -4, 2]}
-    compare_dicts(result, expected)
-
-    with pytest.raises(ValueError, match="No items"):
-        nw.concat([], how="vertical")
-
-    with pytest.raises((Exception, TypeError), match="unable to vstack"):
-        nw.concat([df_left, df_right.rename({"d": "i"})], how="vertical").collect()
-
-
 def test_lazy(constructor: Any) -> None:
     df = nw.from_native(constructor({"a": [1, 2, 3]}), eager_only=True)
     result = df.lazy()
@@ -249,37 +196,6 @@ def test_reindex(df_raw: Any) -> None:
         nw.to_native(df.with_columns(nw.all() + nw.all()))
 
 
-@pytest.mark.parametrize(
-    ("row", "column", "expected"),
-    [(0, 2, 7), (1, "z", 8)],
-)
-def test_item(
-    constructor: Any, row: int | None, column: int | str | None, expected: Any
-) -> None:
-    df = nw.from_native(constructor(data), eager_only=True)
-    assert df.item(row, column) == expected
-    assert df.select("a").head(1).item() == 1
-
-
-@pytest.mark.parametrize(
-    ("row", "column", "err_msg"),
-    [
-        (0, None, re.escape("cannot call `.item()` with only one of `row` or `column`")),
-        (None, 0, re.escape("cannot call `.item()` with only one of `row` or `column`")),
-        (
-            None,
-            None,
-            re.escape("can only call `.item()` if the dataframe is of shape (1, 1)"),
-        ),
-    ],
-)
-def test_item_value_error(
-    constructor: Any, row: int | None, column: int | str | None, err_msg: str
-) -> None:
-    with pytest.raises(ValueError, match=err_msg):
-        nw.from_native(constructor(data), eager_only=True).item(row, column)
-
-
 def test_with_columns_order(constructor: Any) -> None:
     df = nw.from_native(constructor(data))
     result = df.with_columns(nw.col("a") + 1, d=nw.col("a") - 1)
diff --git a/tests/hypothesis/test_join.py b/tests/hypothesis/test_join.py
index 850caa1a..1111959b 100644
--- a/tests/hypothesis/test_join.py
+++ b/tests/hypothesis/test_join.py
@@ -136,6 +136,7 @@ def test_cross_join(  # pragma: no cover
     ),
 )
 @pytest.mark.slow()
+@pytest.mark.filterwarnings("ignore:the default coalesce behavior")
 def test_left_join(  # pragma: no cover
     a_left_data: list[int],
     b_left_data: list[int],
diff --git a/tests/series_only/arithmetic_test.py b/tests/series_only/arithmetic_test.py
index 4668b8ad..c82af4f7 100644
--- a/tests/series_only/arithmetic_test.py
+++ b/tests/series_only/arithmetic_test.py
@@ -65,6 +65,9 @@ def test_truediv_same_dims(constructor_series: Any, request: Any) -> None:
     left=st.integers(-100, 100),
     right=st.integers(-100, 100),
 )
+@pytest.mark.skipif(
+    parse_version(pd.__version__) < (2, 0), reason="convert_dtypes not available"
+)
 def test_mod(left: int, right: int) -> None:
     # hypothesis complains if we add `constructor` as an argument, so this
     # test is a bit manual unfortunately
diff --git a/tests/test_group_by.py b/tests/test_group_by.py
index ff2a8422..df3f314f 100644
--- a/tests/test_group_by.py
+++ b/tests/test_group_by.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+from contextlib import nullcontext
 from typing import Any
 
 import pandas as pd
@@ -8,6 +9,7 @@ import pyarrow as pa
 import pytest
 
 import narwhals.stable.v1 as nw
+from narwhals.utils import parse_version
 from tests.utils import compare_dicts
 
 data = {"a": [1, 1, 3], "b": [4, 4, 6], "c": [7.0, 8, 9]}
@@ -155,13 +157,22 @@ def test_group_by_multiple_keys(constructor: Any) -> None:
 
 
 def test_key_with_nulls(constructor: Any) -> None:
-    data = {"b": [4, 5, None], "a": [1, 2, 3]}
-    result = (
-        nw.from_native(constructor(data))
-        .group_by("b")
-        .agg(nw.len(), nw.col("a").min())
-        .sort("a")
-        .with_columns(nw.col("b").cast(nw.Float64))
+    context = (
+        pytest.raises(NotImplementedError, match="null values")
+        if (
+            "pandas_constructor" in str(constructor)
+            and parse_version(pd.__version__) < parse_version("1.0.0")
+        )
+        else nullcontext()
     )
-    expected = {"b": [4.0, 5, float("nan")], "len": [1, 1, 1], "a": [1, 2, 3]}
-    compare_dicts(result, expected)
+    data = {"b": [4, 5, None], "a": [1, 2, 3]}
+    with context:
+        result = (
+            nw.from_native(constructor(data))
+            .group_by("b")
+            .agg(nw.len(), nw.col("a").min())
+            .sort("a")
+            .with_columns(nw.col("b").cast(nw.Float64))
+        )
+        expected = {"b": [4.0, 5, float("nan")], "len": [1, 1, 1], "a": [1, 2, 3]}
+        compare_dicts(result, expected)