Repository: narwhals-dev/narwhals
Pull request #2823 (2025-07-13 11:45:04)

enh: support `offset_by` for cudf
closes #2801 

<!--
# 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/.github/workflows/extremes.yml b/.github/workflows/extremes.yml
index be33c24f..a836ed00 100644
--- a/.github/workflows/extremes.yml
+++ b/.github/workflows/extremes.yml
@@ -25,7 +25,7 @@ jobs:
           cache-suffix: min-versions-${{ matrix.python-version }}
           cache-dependency-glob: "pyproject.toml"
       - name: install-minimum-versions
-        run: uv pip install pipdeptree tox virtualenv setuptools pandas==1.1.3 polars==0.20.4 numpy==1.19.3 pyarrow==11.0.0 "pyarrow-stubs<17" scipy==1.6.0 scikit-learn==1.1.0 duckdb==1.0 tzdata --system
+        run: uv pip install pipdeptree tox virtualenv setuptools pandas==1.1.3 polars==0.20.4 numpy==1.19.3 pyarrow==13.0.0 "pyarrow-stubs<17" scipy==1.6.0 scikit-learn==1.1.0 duckdb==1.0 tzdata --system
       - name: install-reqs
         run: |
           uv pip install -e . --group tests --system
@@ -37,7 +37,7 @@ jobs:
           echo "$DEPS" | grep 'pandas==1.1.3'
           echo "$DEPS" | grep 'polars==0.20.4'
           echo "$DEPS" | grep 'numpy==1.19.3'
-          echo "$DEPS" | grep 'pyarrow==11.0.0'
+          echo "$DEPS" | grep 'pyarrow==13.0.0'
           echo "$DEPS" | grep 'scipy==1.6.0'
           echo "$DEPS" | grep 'scikit-learn==1.1.0'
           echo "$DEPS" | grep 'duckdb==1.0'
@@ -62,7 +62,7 @@ jobs:
           cache-suffix: pretty-old-versions-${{ matrix.python-version }}
           cache-dependency-glob: "pyproject.toml"
       - name: install-pretty-old-versions
-        run: uv pip install pipdeptree tox virtualenv setuptools pandas==1.1.5 polars==0.20.4 numpy==1.19.3 pyarrow==11.0.0 "pyarrow-stubs<17" scipy==1.6.0 scikit-learn==1.1.0 duckdb==1.0 tzdata --system
+        run: uv pip install pipdeptree tox virtualenv setuptools pandas==1.1.5 polars==0.20.4 numpy==1.19.3 pyarrow==14.0.0 "pyarrow-stubs<17" scipy==1.6.0 scikit-learn==1.1.0 duckdb==1.0 tzdata --system
       - name: install-reqs
         run: uv pip install -e . --group tests --system
       - name: show-deps
@@ -75,7 +75,7 @@ jobs:
           echo "$DEPS" | grep 'pandas==1.1.5'
           echo "$DEPS" | grep 'polars==0.20.4'
           echo "$DEPS" | grep 'numpy==1.19.3'
-          echo "$DEPS" | grep 'pyarrow==11.0.0'
+          echo "$DEPS" | grep 'pyarrow==14.0.0'
           echo "$DEPS" | grep 'scipy==1.6.0'
           echo "$DEPS" | grep 'scikit-learn==1.1.0'
           echo "$DEPS" | grep 'duckdb==1.0'
diff --git a/README.md b/README.md
index ca9da4e6..8ccea314 100644
--- a/README.md
+++ b/README.md
@@ -171,6 +171,7 @@ Join the party!
 - [altair](https://github.com/vega/altair/)
 - [bokeh](https://github.com/bokeh/bokeh)
 - [darts](https://github.com/unit8co/darts)
+- [formulaic](https://github.com/matthewwardrop/formulaic)
 - [hierarchicalforecast](https://github.com/Nixtla/hierarchicalforecast)
 - [marimo](https://github.com/marimo-team/marimo)
 - [metalearners](https://github.com/Quantco/metalearners)
diff --git a/docs/ecosystem.md b/docs/ecosystem.md
index 45a667b9..de79b18f 100644
--- a/docs/ecosystem.md
+++ b/docs/ecosystem.md
@@ -8,6 +8,7 @@ for their dataframe interoperability needs:
 * [altair](https://github.com/vega/altair/)
 * [bokeh](https://github.com/bokeh/bokeh)
 * [darts](https://github.com/unit8co/darts)
+* [formulaic](https://github.com/matthewwardrop/formulaic)
 * [hierarchicalforecast](https://github.com/Nixtla/hierarchicalforecast)
 * [marimo](https://github.com/marimo-team/marimo)
 * [metalearners](https://github.com/Quantco/metalearners)
diff --git a/narwhals/_arrow/group_by.py b/narwhals/_arrow/group_by.py
index 342bdbdc..76512c9d 100644
--- a/narwhals/_arrow/group_by.py
+++ b/narwhals/_arrow/group_by.py
@@ -122,12 +122,6 @@ class ArrowGroupBy(EagerGroupBy["ArrowDataFrame", "ArrowExpr", "Aggregation"]):
         ]
         new_column_names = [new_column_names[i] for i in index_map]
         result_simple = result_simple.rename_columns(new_column_names)
-        if self.compliant._backend_version < (12, 0, 0):
-            columns = result_simple.column_names
-            result_simple = result_simple.select(
-                [*self._keys, *[col for col in columns if col not in self._keys]]
-            )
-
         return self.compliant._with_native(result_simple).rename(
             dict(zip(self._keys, self._output_key_names))
         )
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 58d5e5fa..e22d20fe 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -29,7 +29,6 @@ from narwhals._utils import (
     generate_temporary_column_name,
     is_list_of,
     not_implemented,
-    requires,
 )
 from narwhals.dependencies import is_numpy_array_1d
 from narwhals.exceptions import InvalidOperationError, ShapeError
@@ -156,7 +155,7 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         )
 
     def _from_scalar(self, value: Any) -> Self:
-        if self._backend_version < (13,) and hasattr(value, "as_py"):
+        if hasattr(value, "as_py"):
             value = value.as_py()
         return super()._from_scalar(value)
 
@@ -866,7 +865,6 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         dtypes = self._version.dtypes
         return (~self.is_null()).cast(dtypes.UInt32()).cum_sum(reverse=reverse)
 
-    @requires.backend_version((13,))
     def cum_min(self, *, reverse: bool) -> Self:
         result = (
             pc.cumulative_min(self.native, skip_nulls=True)
@@ -875,7 +873,6 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         )
         return self._with_native(result)
 
-    @requires.backend_version((13,))
     def cum_max(self, *, reverse: bool) -> Self:
         result = (
             pc.cumulative_max(self.native, skip_nulls=True)
@@ -884,7 +881,6 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         )
         return self._with_native(result)
 
-    @requires.backend_version((13,))
     def cum_prod(self, *, reverse: bool) -> Self:
         result = (
             pc.cumulative_prod(self.native, skip_nulls=True)
@@ -1023,7 +1019,6 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         result = pc.if_else(null_mask, lit(None, rank.type), rank)
         return self._with_native(result)
 
-    @requires.backend_version((13,))
     def hist(  # noqa: C901, PLR0912, PLR0915
         self,
         bins: list[float | int] | None,
diff --git a/narwhals/_arrow/series_dt.py b/narwhals/_arrow/series_dt.py
index 7c1c2bfb..3e58aa3f 100644
--- a/narwhals/_arrow/series_dt.py
+++ b/narwhals/_arrow/series_dt.py
@@ -213,13 +213,14 @@ class ArrowSeriesDateTimeNamespace(ArrowSeriesNamespace):
         if interval.unit in {"y", "q", "mo"}:
             msg = f"Offsetting by {interval.unit} is not yet supported for pyarrow."
             raise NotImplementedError(msg)
-        if interval.unit == "d":
+        dtype = self.compliant.dtype
+        datetime_dtype = self.version.dtypes.Datetime
+        if interval.unit == "d" and isinstance(dtype, datetime_dtype) and dtype.time_zone:
             offset: pa.DurationScalar[Any] = lit(interval.to_timedelta())
-            if time_zone := native.type.tz:
-                native_naive = pc.local_timestamp(native)
-                result = pc.assume_timezone(pc.add(native_naive, offset), time_zone)
-                return self.with_native(result)
-        elif interval.unit == "ns":  # pragma: no cover
+            native_naive = pc.local_timestamp(native)
+            result = pc.assume_timezone(pc.add(native_naive, offset), dtype.time_zone)
+            return self.with_native(result)
+        if interval.unit == "ns":  # pragma: no cover
             offset = lit(interval.multiple, pa.duration("ns"))  # type: ignore[assignment]
         else:
             offset = lit(interval.to_timedelta())
diff --git a/narwhals/_arrow/series_str.py b/narwhals/_arrow/series_str.py
index b20623d7..0141bc39 100644
--- a/narwhals/_arrow/series_str.py
+++ b/narwhals/_arrow/series_str.py
@@ -70,13 +70,9 @@ class ArrowSeriesStringNamespace(ArrowSeriesNamespace):
         binary_join: Incomplete = pc.binary_join_element_wise
         native = self.native
         hyphen, plus = lit("-"), lit("+")
-
-        _slice_length: int | None = (
-            self.len_chars().max() if self.backend_version < (13, 0) else None
-        )
         first_char, remaining_chars = (
             self.slice(0, 1).native,
-            self.slice(1, _slice_length).native,
+            self.slice(1, None).native,
         )
 
         # Conditions
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index 8529eccc..1f5e11b8 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -295,9 +295,8 @@ def cast_for_truediv(
     if pa.types.is_integer(arrow_array.type) and pa.types.is_integer(pa_object.type):
         # GH: 56645.  # noqa: ERA001
         # https://github.com/apache/arrow/issues/35563
-        # NOTE: `pyarrow==11.*` doesn't allow keywords in `Array.cast`
-        return pc.cast(arrow_array, pa.float64(), safe=False), pc.cast(
-            pa_object, pa.float64(), safe=False
+        return arrow_array.cast(pa.float64(), safe=False), pa_object.cast(
+            pa.float64(), safe=False
         )
 
     return arrow_array, pa_object
diff --git a/narwhals/_pandas_like/series_dt.py b/narwhals/_pandas_like/series_dt.py
index 3c7f7b9a..51b3fd2d 100644
--- a/narwhals/_pandas_like/series_dt.py
+++ b/narwhals/_pandas_like/series_dt.py
@@ -2,8 +2,6 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING, Any
 
-import pandas as pd
-
 from narwhals._compliant.any_namespace import DateTimeNamespace
 from narwhals._constants import (
     EPOCH_YEAR,
@@ -25,6 +23,10 @@ from narwhals._pandas_like.utils import (
 )
 
 if TYPE_CHECKING:
+    from datetime import timedelta
+
+    import pandas as pd
+
     from narwhals._pandas_like.series import PandasLikeSeries
     from narwhals.typing import TimeUnit
 
@@ -246,10 +248,8 @@ class PandasLikeSeriesDateTimeNamespace(
         )
 
     def offset_by(self, by: str) -> PandasLikeSeries:
-        if self.implementation.is_cudf():
-            msg = "Not implemented for cuDF."
-            raise NotImplementedError(msg)
         native = self.native
+        pdx = self.compliant.__native_namespace__()
         if self._is_pyarrow():
             import pyarrow as pa  # ignore-banned-import
 
@@ -270,19 +270,21 @@ class PandasLikeSeriesDateTimeNamespace(
             if unit == "q":
                 multiple *= 3
                 unit = "mo"
-            offset: pd.DateOffset | pd.Timedelta
+            offset: pd.DateOffset | timedelta
             if unit == "y":
-                offset = pd.DateOffset(years=multiple)
+                offset = pdx.DateOffset(years=multiple)
             elif unit == "mo":
-                offset = pd.DateOffset(months=multiple)
+                offset = pdx.DateOffset(months=multiple)
+            elif unit == "ns":
+                offset = pdx.Timedelta(multiple, unit=UNITS_DICT[unit])
             else:
-                offset = pd.Timedelta(multiple, unit=UNITS_DICT[unit])  # type: ignore[arg-type]
-            if unit == "d":
-                original_timezone = native.dt.tz
+                offset = interval.to_timedelta()
+            dtype = self.compliant.dtype
+            datetime_dtype = self.version.dtypes.Datetime
+            if unit == "d" and isinstance(dtype, datetime_dtype) and dtype.time_zone:
                 native_without_timezone = native.dt.tz_localize(None)
                 result_pd = native_without_timezone + offset
-                if original_timezone is not None:
-                    result_pd = result_pd.dt.tz_localize(original_timezone)
+                result_pd = result_pd.dt.tz_localize(dtype.time_zone)
             else:
                 result_pd = native + offset
 
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 65c15e72..29fc9d4d 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -508,7 +508,7 @@ def narwhals_to_native_dtype(  # noqa: C901, PLR0912, PLR0915
         try:
             import pyarrow as pa  # ignore-banned-import
         except ModuleNotFoundError:  # pragma: no cover
-            msg = "'pyarrow>=11.0.0' is required for `Date` dtype."
+            msg = "'pyarrow>=13.0.0' is required for `Date` dtype."
         return "date32[pyarrow]"
     if isinstance_or_issubclass(dtype, dtypes.Enum):
         if version is Version.V1:
diff --git a/narwhals/_utils.py b/narwhals/_utils.py
index ee9ff07f..10314654 100644
--- a/narwhals/_utils.py
+++ b/narwhals/_utils.py
@@ -595,7 +595,7 @@ MIN_VERSIONS: Mapping[Implementation, tuple[int, ...]] = {
     Implementation.PANDAS: (1, 1, 3),
     Implementation.MODIN: (0, 8, 2),
     Implementation.CUDF: (24, 10),
-    Implementation.PYARROW: (11,),
+    Implementation.PYARROW: (13,),
     Implementation.PYSPARK: (3, 5),
     Implementation.PYSPARK_CONNECT: (3, 5),
     Implementation.POLARS: (0, 20, 4),
diff --git a/pyproject.toml b/pyproject.toml
index 972e429b..da2fb5ca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,7 +37,7 @@ Repository = "https://github.com/narwhals-dev/narwhals"
 pandas = ["pandas>=1.1.3"]
 modin = ["modin"]
 cudf = ["cudf>=24.10.0"]
-pyarrow = ["pyarrow>=11.0.0"]
+pyarrow = ["pyarrow>=13.0.0"]
 pyspark = ["pyspark>=3.5.0"]
 pyspark-connect = ["pyspark[connect]>=3.5.0"]
 polars = ["polars>=0.20.4"]
diff --git a/utils/generate_random_versions.py b/utils/generate_random_versions.py
index 30489be5..342fdda9 100644
--- a/utils/generate_random_versions.py
+++ b/utils/generate_random_versions.py
@@ -38,9 +38,6 @@ POLARS_VERSION = [
     "1.1.0",
 ]
 PYARROW_VERSION = [
-    "11.0.0",
-    "12.0.0",
-    "12.0.1",
     "13.0.0",
     "14.0.0",
     "14.0.1",
@@ -50,6 +47,9 @@ PYARROW_VERSION = [
     "15.0.2",
     "16.0.0",
     "16.1.0",
+    "17.0.0",
+    "18.0.0",
+    "18.1.0",
 ]
 
 pandas_version, numpy_version = random.choice(PANDAS_AND_NUMPY_VERSION)


Test patch:
diff --git a/tests/expr_and_series/cum_max_test.py b/tests/expr_and_series/cum_max_test.py
index d23742d7..eb273cef 100644
--- a/tests/expr_and_series/cum_max_test.py
+++ b/tests/expr_and_series/cum_max_test.py
@@ -7,7 +7,6 @@ from tests.utils import (
     DUCKDB_VERSION,
     PANDAS_VERSION,
     POLARS_VERSION,
-    PYARROW_VERSION,
     Constructor,
     ConstructorEager,
     assert_equal_data,
@@ -22,12 +21,7 @@ expected = {"cum_max": [1, 3, None, 3], "reverse_cum_max": [3, 3, None, 2]}
 def test_cum_max_expr(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager, *, reverse: bool
 ) -> None:
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor_eager):
-        request.applymarker(pytest.mark.xfail)
-
-    if (PANDAS_VERSION < (2, 1) or PYARROW_VERSION < (13,)) and "pandas_pyarrow" in str(
-        constructor_eager
-    ):
+    if (PANDAS_VERSION < (2, 1)) and "pandas_pyarrow" in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
 
     name = "reverse_cum_max" if reverse else "cum_max"
@@ -141,8 +135,6 @@ def test_lazy_cum_max_ungrouped(
         "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3)
     ):
         pytest.skip(reason="too old version")
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
 
     df = nw.from_native(
         constructor({"a": [2, 3, 1], "b": [0, 2, 1], "i": [1, 2, 0]})
@@ -174,8 +166,6 @@ def test_lazy_cum_max_ungrouped_ordered_by_nulls(
         "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3)
     ):
         pytest.skip(reason="too old version")
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
 
     df = nw.from_native(
         constructor(
@@ -200,12 +190,7 @@ def test_lazy_cum_max_ungrouped_ordered_by_nulls(
 def test_cum_max_series(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager
 ) -> None:
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor_eager):
-        request.applymarker(pytest.mark.xfail)
-
-    if (PANDAS_VERSION < (2, 1) or PYARROW_VERSION < (13,)) and "pandas_pyarrow" in str(
-        constructor_eager
-    ):
+    if (PANDAS_VERSION < (2, 1)) and "pandas_pyarrow" 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/cum_min_test.py b/tests/expr_and_series/cum_min_test.py
index 7f68580f..3796a284 100644
--- a/tests/expr_and_series/cum_min_test.py
+++ b/tests/expr_and_series/cum_min_test.py
@@ -7,7 +7,6 @@ from tests.utils import (
     DUCKDB_VERSION,
     PANDAS_VERSION,
     POLARS_VERSION,
-    PYARROW_VERSION,
     Constructor,
     ConstructorEager,
     assert_equal_data,
@@ -22,12 +21,7 @@ expected = {"cum_min": [3, 1, None, 1], "reverse_cum_min": [1, 1, None, 2]}
 def test_cum_min_expr(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager, *, reverse: bool
 ) -> None:
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor_eager):
-        request.applymarker(pytest.mark.xfail)
-
-    if (PANDAS_VERSION < (2, 1) or PYARROW_VERSION < (13,)) and "pandas_pyarrow" in str(
-        constructor_eager
-    ):
+    if (PANDAS_VERSION < (2, 1)) and "pandas_pyarrow" in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
 
     name = "reverse_cum_min" if reverse else "cum_min"
@@ -141,9 +135,6 @@ def test_lazy_cum_min_ungrouped(
         "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3)
     ):
         pytest.skip(reason="too old version")
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     df = nw.from_native(
         constructor({"a": [2, 3, 1], "b": [0, 2, 1], "i": [1, 2, 0]})
     ).sort("i")
@@ -174,9 +165,6 @@ def test_lazy_cum_min_ungrouped_ordered_by_nulls(
         "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3)
     ):
         pytest.skip(reason="too old version")
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     df = nw.from_native(
         constructor(
             {
@@ -200,12 +188,7 @@ def test_lazy_cum_min_ungrouped_ordered_by_nulls(
 def test_cum_min_series(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager
 ) -> None:
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor_eager):
-        request.applymarker(pytest.mark.xfail)
-
-    if (PANDAS_VERSION < (2, 1) or PYARROW_VERSION < (13,)) and "pandas_pyarrow" in str(
-        constructor_eager
-    ):
+    if (PANDAS_VERSION < (2, 1)) and "pandas_pyarrow" 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/cum_prod_test.py b/tests/expr_and_series/cum_prod_test.py
index d626403a..dbc4c5a9 100644
--- a/tests/expr_and_series/cum_prod_test.py
+++ b/tests/expr_and_series/cum_prod_test.py
@@ -7,7 +7,6 @@ from tests.utils import (
     DUCKDB_VERSION,
     PANDAS_VERSION,
     POLARS_VERSION,
-    PYARROW_VERSION,
     Constructor,
     ConstructorEager,
     assert_equal_data,
@@ -22,12 +21,7 @@ expected = {"cum_prod": [1, 2, None, 6], "reverse_cum_prod": [6, 6, None, 3]}
 def test_cum_prod_expr(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager, *, reverse: bool
 ) -> None:
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor_eager):
-        request.applymarker(pytest.mark.xfail)
-
-    if (PANDAS_VERSION < (2, 1) or PYARROW_VERSION < (13,)) and "pandas_pyarrow" in str(
-        constructor_eager
-    ):
+    if (PANDAS_VERSION < (2, 1)) and "pandas_pyarrow" in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
 
     name = "reverse_cum_prod" if reverse else "cum_prod"
@@ -40,12 +34,7 @@ def test_cum_prod_expr(
 def test_cum_prod_series(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager
 ) -> None:
-    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor_eager):
-        request.applymarker(pytest.mark.xfail)
-
-    if (PANDAS_VERSION < (2, 1) or PYARROW_VERSION < (13,)) and "pandas_pyarrow" in str(
-        constructor_eager
-    ):
+    if (PANDAS_VERSION < (2, 1)) and "pandas_pyarrow" 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/diff_test.py b/tests/expr_and_series/diff_test.py
index 98aca41b..dc744054 100644
--- a/tests/expr_and_series/diff_test.py
+++ b/tests/expr_and_series/diff_test.py
@@ -6,7 +6,6 @@ import narwhals as nw
 from tests.utils import (
     DUCKDB_VERSION,
     POLARS_VERSION,
-    PYARROW_VERSION,
     Constructor,
     ConstructorEager,
     assert_equal_data,
@@ -16,9 +15,6 @@ data = {"i": [0, 1, 2, 3, 4], "b": [1, 2, 3, 5, 3], "c": [5, 4, 3, 2, 1]}
 
 
 def test_diff(constructor_eager: ConstructorEager) -> None:
-    if "pyarrow_table_constructor" in str(constructor_eager) and PYARROW_VERSION < (13,):
-        # pc.pairwisediff is available since pyarrow 13.0.0
-        pytest.skip()
     df = nw.from_native(constructor_eager(data))
     result = df.with_columns(c_diff=nw.col("c").diff()).filter(nw.col("i") > 0)
     expected = {
@@ -32,9 +28,6 @@ def test_diff(constructor_eager: ConstructorEager) -> None:
 
 def test_diff_lazy(constructor: Constructor) -> None:
     data = {"i": [None, 1, 2, 3, 4], "b": [1, 2, 3, 5, 3], "c": [5, 4, 3, 2, 1]}
-    if "pyarrow_table_constructor" in str(constructor) and PYARROW_VERSION < (13,):
-        # pc.pairwisediff is available since pyarrow 13.0.0
-        pytest.skip()
     if "polars" in str(constructor) and POLARS_VERSION < (1, 10):
         pytest.skip()
     if "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3):
@@ -55,9 +48,6 @@ def test_diff_lazy(constructor: Constructor) -> None:
 def test_diff_lazy_grouped(
     constructor: Constructor, request: pytest.FixtureRequest
 ) -> None:
-    if "pyarrow_table_constructor" in str(constructor) and PYARROW_VERSION < (13,):
-        # pc.pairwisediff is available since pyarrow 13.0.0
-        pytest.skip()
     if "polars" in str(constructor) and POLARS_VERSION < (1, 10):
         pytest.skip()
     if "duckdb" in str(constructor) and DUCKDB_VERSION < (1, 3):
@@ -83,12 +73,7 @@ def test_diff_lazy_grouped(
     assert_equal_data(result, expected)
 
 
-def test_diff_series(
-    constructor_eager: ConstructorEager, request: pytest.FixtureRequest
-) -> None:
-    if "pyarrow_table_constructor" in str(constructor_eager) and PYARROW_VERSION < (13,):
-        # pc.pairwisediff is available since pyarrow 13.0.0
-        request.applymarker(pytest.mark.xfail)
+def test_diff_series(constructor_eager: ConstructorEager) -> None:
     df = nw.from_native(constructor_eager(data), eager_only=True)
     expected = {
         "i": [1, 2, 3, 4],
diff --git a/tests/expr_and_series/dt/convert_time_zone_test.py b/tests/expr_and_series/dt/convert_time_zone_test.py
index 70cdb143..ba15f188 100644
--- a/tests/expr_and_series/dt/convert_time_zone_test.py
+++ b/tests/expr_and_series/dt/convert_time_zone_test.py
@@ -10,7 +10,6 @@ import narwhals as nw
 from tests.utils import (
     PANDAS_VERSION,
     POLARS_VERSION,
-    PYARROW_VERSION,
     Constructor,
     assert_equal_data,
     is_windows,
@@ -86,7 +85,6 @@ def test_convert_time_zone_from_none(
         or ("pyarrow_table" in str(constructor) and is_windows())
         or ("pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1))
         or ("modin_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1))
-        or ("pyarrow_table" in str(constructor) and PYARROW_VERSION < (12,))
     ):
         pytest.skip()
     if any(x in str(constructor) for x in ("cudf", "duckdb", "pyspark", "ibis")):
diff --git a/tests/expr_and_series/dt/offset_by_test.py b/tests/expr_and_series/dt/offset_by_test.py
index a5dfb84d..5d1c6f7b 100644
--- a/tests/expr_and_series/dt/offset_by_test.py
+++ b/tests/expr_and_series/dt/offset_by_test.py
@@ -1,13 +1,12 @@
 from __future__ import annotations
 
-from datetime import datetime, timezone
+from datetime import date, datetime, timezone
 
 import pytest
 
 import narwhals as nw
 from tests.utils import (
     PANDAS_VERSION,
-    PYARROW_VERSION,
     Constructor,
     ConstructorEager,
     assert_equal_data,
@@ -33,6 +32,13 @@ data_dst = {"a": [datetime(2020, 10, 25, tzinfo=timezone.utc)]}
                 datetime(2020, 1, 2, 2, 4, 14, 715125),
             ],
         ),
+        (
+            "2000ns",
+            [
+                datetime(2021, 3, 1, 12, 34, 56, 49014),
+                datetime(2020, 1, 2, 2, 4, 14, 715125),
+            ],
+        ),
         (
             "2ms",
             [
@@ -108,11 +114,12 @@ def test_offset_by(
         x in str(constructor) for x in ("dask", "pyarrow", "ibis")
     ):
         request.applymarker(pytest.mark.xfail())
+    if "ns" in by and any(
+        x in str(constructor) for x in ("dask", "pyspark", "ibis", "cudf", "duckdb")
+    ):
+        request.applymarker(pytest.mark.xfail())
     if by.endswith("d") and any(x in str(constructor) for x in ("dask", "ibis")):
         request.applymarker(pytest.mark.xfail())
-    if "cudf" in str(constructor):
-        # https://github.com/rapidsai/cudf/issues/19292
-        request.applymarker(pytest.mark.xfail)
     result = df.select(nw.col("a").dt.offset_by(by))
     assert_equal_data(result, {"a": expected})
 
@@ -120,74 +127,65 @@ def test_offset_by(
 @pytest.mark.parametrize(
     ("by", "expected"),
     [
-        ("2d", ["2024-01-03T05:45+0545"]),
-        ("5mo", ["2024-06-01T05:45+0545"]),
-        ("7q", ["2025-10-01T05:45+0545"]),
-        ("5y", ["2029-01-01T05:45+0545"]),
+        ("2d", "2024-01-03T05:45+0545"),
+        ("5mo", "2024-06-01T05:45+0545"),
+        ("7q", "2025-10-01T05:45+0545"),
+        ("5y", "2029-01-01T05:45+0545"),
     ],
 )
 def test_offset_by_tz(
-    request: pytest.FixtureRequest,
-    constructor: Constructor,
-    by: str,
-    expected: list[datetime],
+    request: pytest.FixtureRequest, constructor: Constructor, by: str, expected: str
 ) -> None:
     if (
         ("pyarrow" in str(constructor) and is_windows())
         or ("pyarrow_table" in str(constructor) and is_windows())
         or ("pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1))
         or ("modin_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1))
-        or ("pyarrow_table" in str(constructor) and PYARROW_VERSION < (12,))
     ):
         pytest.skip()
     if any(x in str(constructor) for x in ("duckdb", "pyspark", "sqlframe", "ibis")):
         # pyspark,duckdb don't support changing time zones.
         # convert_time_zone is not supported for ibis.
         request.applymarker(pytest.mark.xfail())
+    if any(x in str(constructor) for x in ("cudf",)) and "d" not in by:
+        # cudf: https://github.com/rapidsai/cudf/issues/19363
+        request.applymarker(pytest.mark.xfail())
     if any(x in by for x in ("y", "q", "mo")) and any(
         x in str(constructor) for x in ("dask", "pyarrow", "ibis")
     ):
         request.applymarker(pytest.mark.xfail())
     if by.endswith("d") and any(x in str(constructor) for x in ("dask",)):
         request.applymarker(pytest.mark.xfail())
-    if "cudf" in str(constructor):
-        # https://github.com/rapidsai/cudf/issues/19292
-        request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data_tz))
     df = df.select(nw.col("a").dt.convert_time_zone("Asia/Kathmandu"))
     result = df.select(nw.col("a").dt.offset_by(by))
-    result_str = result.select(nw.col("a").dt.to_string("%Y-%m-%dT%H:%M%z"))
-    assert_equal_data(result_str, {"a": expected})
+    assert_equal_data(result, {"a": [datetime.strptime(expected, "%Y-%m-%dT%H:%M%z")]})
 
 
 @pytest.mark.parametrize(
     ("by", "expected"),
     [
-        ("2d", ["2020-10-27T02:00+0100"]),
-        ("5mo", ["2021-03-25T02:00+0100"]),
-        ("1q", ["2021-01-25T02:00+0100"]),
+        ("2d", "2020-10-27T02:00+0100"),
+        ("5mo", "2021-03-25T02:00+0100"),
+        ("1q", "2021-01-25T02:00+0100"),
     ],
 )
 def test_offset_by_dst(
-    request: pytest.FixtureRequest,
-    constructor: Constructor,
-    by: str,
-    expected: list[datetime],
+    request: pytest.FixtureRequest, constructor: Constructor, by: str, expected: str
 ) -> None:
     if (
         ("pyarrow" in str(constructor) and is_windows())
         or ("pyarrow_table" in str(constructor) and is_windows())
         or ("pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1))
         or ("modin_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 1))
-        or ("pyarrow_table" in str(constructor) and PYARROW_VERSION < (12,))
     ):
         pytest.skip()
-    if any(
-        x in str(constructor) for x in ("duckdb", "sqlframe", "pyspark", "ibis", "cudf")
-    ):
+    if any(x in str(constructor) for x in ("duckdb", "sqlframe", "pyspark", "ibis")):
         # pyspark,duckdb don't support changing time zones.
         # convert_time_zone is not supported for ibis.
-        # cudf https://github.com/rapidsai/cudf/issues/19292
+        request.applymarker(pytest.mark.xfail())
+    if any(x in str(constructor) for x in ("cudf",)) and "d" not in by:
+        # cudf: https://github.com/rapidsai/cudf/issues/19363
         request.applymarker(pytest.mark.xfail())
     if any(x in by for x in ("y", "q", "mo")) and any(
         x in str(constructor) for x in ("dask", "pyarrow")
@@ -198,16 +196,10 @@ def test_offset_by_dst(
     df = nw.from_native(constructor(data_dst))
     df = df.with_columns(a=nw.col("a").dt.convert_time_zone("Europe/Amsterdam"))
     result = df.select(nw.col("a").dt.offset_by(by))
-    result_str = result.select(nw.col("a").dt.to_string("%Y-%m-%dT%H:%M%z"))
-    assert_equal_data(result_str, {"a": expected})
+    assert_equal_data(result, {"a": [datetime.strptime(expected, "%Y-%m-%dT%H:%M%z")]})
 
 
-def test_offset_by_series(
-    constructor_eager: ConstructorEager, request: pytest.FixtureRequest
-) -> None:
-    if "cudf" in str(constructor_eager):
-        # https://github.com/rapidsai/cudf/issues/19292
-        request.applymarker(pytest.mark.xfail)
+def test_offset_by_series(constructor_eager: ConstructorEager) -> None:
     df = nw.from_native(constructor_eager(data), eager_only=True)
     result = df.select(df["a"].dt.offset_by("1h"))
     expected = {
@@ -219,13 +211,23 @@ def test_offset_by_series(
     assert_equal_data(result, expected)
 
 
-def test_offset_by_invalid_interval(
-    constructor: Constructor, request: pytest.FixtureRequest
-) -> None:
-    if "cudf" in str(constructor):
-        # https://github.com/rapidsai/cudf/issues/19292
-        request.applymarker(pytest.mark.xfail)
+def test_offset_by_invalid_interval(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     msg = "Invalid `every` string"
     with pytest.raises(ValueError, match=msg):
         df.select(nw.col("a").dt.offset_by("1r"))
+
+
+@pytest.mark.skipif(PANDAS_VERSION < (2, 2), reason="too old for pyarrow date type")
+def test_offset_by_date_pandas() -> None:
+    pytest.importorskip("pandas")
+    import pandas as pd
+
+    df = nw.from_native(pd.DataFrame({"a": [date(2020, 1, 1)]}, dtype="date32[pyarrow]"))
+    result = df.select(nw.col("a").dt.offset_by("1d"))
+    expected = {"a": [date(2020, 1, 2)]}
+    assert_equal_data(result, expected)
+    df = nw.from_native(pd.DataFrame({"a": [date(2020, 1, 1)]}))
+    result = df.select(nw.col("a").dt.offset_by("1d"))
+    expected = {"a": [date(2020, 1, 2)]}
+    assert_equal_data(result, expected)
diff --git a/tests/expr_and_series/dt/replace_time_zone_test.py b/tests/expr_and_series/dt/replace_time_zone_test.py
index 748347ef..11734fce 100644
--- a/tests/expr_and_series/dt/replace_time_zone_test.py
+++ b/tests/expr_and_series/dt/replace_time_zone_test.py
@@ -7,13 +7,7 @@ from zoneinfo import ZoneInfo
 import pytest
 
 import narwhals as nw
-from tests.utils import (
-    PANDAS_VERSION,
-    PYARROW_VERSION,
-    Constructor,
-    assert_equal_data,
-    is_windows,
-)
+from tests.utils import PANDAS_VERSION, Constructor, assert_equal_data, is_windows
 
 if TYPE_CHECKING:
     from tests.utils import ConstructorEager
@@ -26,7 +20,6 @@ def test_replace_time_zone(
         ("pyarrow" in str(constructor) and is_windows())
         or ("pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2,))
         or ("modin_pyarrow" in str(constructor) and PANDAS_VERSION < (2,))
-        or ("pyarrow_table" in str(constructor) and PYARROW_VERSION < (12,))
     ):
         pytest.skip()
 
@@ -54,7 +47,6 @@ def test_replace_time_zone_none(constructor: Constructor) -> None:
         ("pyarrow" in str(constructor) and is_windows())
         or ("pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2,))
         or ("modin_pyarrow" in str(constructor) and PANDAS_VERSION < (2,))
-        or ("pyarrow_table" in str(constructor) and PYARROW_VERSION < (12,))
     ):
         pytest.skip()
     data = {
@@ -81,7 +73,6 @@ def test_replace_time_zone_series(
         ("pyarrow" in str(constructor_eager) and is_windows())
         or ("pandas_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2,))
         or ("modin_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2,))
-        or ("pyarrow_table" in str(constructor_eager) and PYARROW_VERSION < (12,))
     ):
         pytest.skip()
     if any(x in str(constructor_eager) for x in ("cudf",)):
@@ -108,7 +99,6 @@ def test_replace_time_zone_none_series(constructor_eager: ConstructorEager) -> N
         ("pyarrow" in str(constructor_eager) and is_windows())
         or ("pandas_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2,))
         or ("modin_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2,))
-        or ("pyarrow_table" in str(constructor_eager) and PYARROW_VERSION < (12,))
     ):
         pytest.skip()
     data = {
diff --git a/tests/expr_and_series/dt/timestamp_test.py b/tests/expr_and_series/dt/timestamp_test.py
index d1ccfa15..35f6a52a 100644
--- a/tests/expr_and_series/dt/timestamp_test.py
+++ b/tests/expr_and_series/dt/timestamp_test.py
@@ -13,7 +13,6 @@ import narwhals as nw
 from tests.utils import (
     PANDAS_VERSION,
     POLARS_VERSION,
-    PYARROW_VERSION,
     Constructor,
     ConstructorEager,
     assert_equal_data,
@@ -109,10 +108,6 @@ def test_timestamp_datetimes_tz_aware(
             "pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2,),
             "Requires pandas >= 2.0 for pyarrow support",
         ),
-        (
-            "pyarrow_table" in str(constructor) and PYARROW_VERSION < (12,),
-            "Requires pyarrow >= 12.0",
-        ),
         (
             "pandas_pyarrow" in str(constructor) and PANDAS_VERSION < (2, 2),
             "Requires pandas >= 2.2 for reliable timestamps",
diff --git a/tests/expr_and_series/str/to_datetime_test.py b/tests/expr_and_series/str/to_datetime_test.py
index 58223c6e..8837ba26 100644
--- a/tests/expr_and_series/str/to_datetime_test.py
+++ b/tests/expr_and_series/str/to_datetime_test.py
@@ -12,7 +12,6 @@ from narwhals._arrow.utils import parse_datetime_format
 from narwhals._pandas_like.utils import get_dtype_backend
 from tests.utils import (
     PANDAS_VERSION,
-    PYARROW_VERSION,
     assert_equal_data,
     is_pyarrow_windows_no_tzdata,
     is_windows,
@@ -205,9 +204,6 @@ def test_pyarrow_infer_datetime_raise_inconsistent_date_fmt(
 def test_to_datetime_tz_aware(
     constructor: Constructor, request: pytest.FixtureRequest, format: str | None
 ) -> None:
-    if "pyarrow_table" in str(constructor) and PYARROW_VERSION < (13,):
-        # bugged
-        pytest.skip()
     if is_pyarrow_windows_no_tzdata(constructor) or (
         "sqlframe" in str(constructor) and format is not None and is_windows()
     ):
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 ba1bb7de..9c439579 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
@@ -3,7 +3,7 @@ from __future__ import annotations
 import pytest
 
 import narwhals as nw
-from tests.utils import PYARROW_VERSION, Constructor, ConstructorEager, assert_equal_data
+from tests.utils import Constructor, ConstructorEager, assert_equal_data
 
 
 @pytest.mark.parametrize(
@@ -24,9 +24,6 @@ from tests.utils import PYARROW_VERSION, Constructor, ConstructorEager, assert_e
 def test_str_to_uppercase(
     constructor: Constructor, data: dict[str, list[str]], expected: dict[str, list[str]]
 ) -> None:
-    if "dask" in str(constructor) and PYARROW_VERSION < (12,):
-        pytest.skip()
-
     if (
         any(
             x in str(constructor)
diff --git a/tests/frame/to_numpy_test.py b/tests/frame/to_numpy_test.py
index 65013b10..bc17e9c5 100644
--- a/tests/frame/to_numpy_test.py
+++ b/tests/frame/to_numpy_test.py
@@ -7,7 +7,7 @@ import numpy as np
 import pytest
 
 import narwhals as nw
-from tests.utils import PANDAS_VERSION, PYARROW_VERSION, is_windows
+from tests.utils import PANDAS_VERSION, is_windows
 
 if TYPE_CHECKING:
     from tests.utils import ConstructorEager
@@ -25,10 +25,8 @@ def test_to_numpy(constructor_eager: ConstructorEager) -> None:
 def test_to_numpy_tz_aware(
     constructor_eager: ConstructorEager, request: pytest.FixtureRequest
 ) -> None:
-    if (
-        ("pyarrow_table" in str(constructor_eager) and PYARROW_VERSION < (12,))
-        or ("pandas_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2, 2))
-        or ("pyarrow" in str(constructor_eager) and is_windows())
+    if ("pandas_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2, 2)) or (
+        "pyarrow" in str(constructor_eager) and is_windows()
     ):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(
diff --git a/tests/selectors_test.py b/tests/selectors_test.py
index 9bdc7053..226560ae 100644
--- a/tests/selectors_test.py
+++ b/tests/selectors_test.py
@@ -85,7 +85,6 @@ def test_datetime(constructor: Constructor, request: pytest.FixtureRequest) -> N
         "pyspark" in str(constructor)
         or "duckdb" in str(constructor)
         or "dask" in str(constructor)
-        or ("pyarrow_table" in str(constructor) and PYARROW_VERSION < (12,))
         or ("pyarrow" in str(constructor) and is_windows())
         or ("pandas" in str(constructor) and PANDAS_VERSION < (2,))
         or "ibis" in str(constructor)
@@ -256,9 +255,6 @@ def test_tz_aware(constructor: Constructor, request: pytest.FixtureRequest) -> N
     if "polars" in str(constructor) and POLARS_VERSION < (1, 19):
         # bug in old polars
         pytest.skip()
-    if "pyarrow_table" in str(constructor) and PYARROW_VERSION < (12,):
-        # bug in old pyarrow
-        pytest.skip()
     if (
         "duckdb" in str(constructor)
         or "pyspark" in str(constructor)
diff --git a/tests/series_only/hist_test.py b/tests/series_only/hist_test.py
index 7ad1ef8e..13244e18 100644
--- a/tests/series_only/hist_test.py
+++ b/tests/series_only/hist_test.py
@@ -12,12 +12,7 @@ from hypothesis import given
 
 import narwhals as nw
 from narwhals.exceptions import ComputeError
-from tests.utils import (
-    POLARS_VERSION,
-    PYARROW_VERSION,
-    ConstructorEager,
-    assert_equal_data,
-)
+from tests.utils import POLARS_VERSION, ConstructorEager, assert_equal_data
 
 rnd = Random(0)  # noqa: S311
 
@@ -68,8 +63,6 @@ def test_hist_bin(
         pa = pytest.importorskip("pyarrow")
         constructor_eager = pa.table
 
-    if library == "pyarrow" and PYARROW_VERSION < (13,):
-        pytest.skip()
     df = nw.from_native(constructor_eager(data)).with_columns(
         float=nw.col("int").cast(nw.Float64)
     )
@@ -131,9 +124,6 @@ def test_hist_count(
     else:
         pa = pytest.importorskip("pyarrow")
         constructor_eager = pa.table
-    if library == "pyarrow" and PYARROW_VERSION < (13,):
-        pytest.skip()
-
     df = nw.from_native(constructor_eager(data)).with_columns(
         float=nw.col("int").cast(nw.Float64)
     )
@@ -184,9 +174,6 @@ def test_hist_count_no_spread(library: str) -> None:
     else:
         pa = pytest.importorskip("pyarrow")
         constructor_eager = pa.table
-    if library == "pyarrow" and PYARROW_VERSION < (13,):
-        pytest.skip()
-
     data = {"all_zero": [0, 0, 0], "all_non_zero": [5, 5, 5]}
     df = nw.from_native(constructor_eager(data))
 
@@ -226,8 +213,6 @@ def test_hist_no_data(library: str, *, include_breakpoint: bool) -> None:
     else:
         pa = pytest.importorskip("pyarrow")
         constructor_eager = pa.table
-    if library == "pyarrow" and PYARROW_VERSION < (13,):
-        pytest.skip()
     s = nw.from_native(constructor_eager({"values": []})).select(
         nw.col("values").cast(nw.Float64)
     )["values"]
@@ -257,8 +242,6 @@ def test_hist_small_bins(library: str) -> None:
     else:
         pa = pytest.importorskip("pyarrow")
         constructor_eager = pa.table
-    if library == "pyarrow" and PYARROW_VERSION < (13,):
-        pytest.skip()
     s = nw.from_native(constructor_eager({"values": [1, 2, 3]}))
     result = s["values"].hist(bins=None, bin_count=None)
     assert len(result) == 10
@@ -370,9 +353,6 @@ def test_hist_count_hypothesis(
     else:
         pa = pytest.importorskip("pyarrow")
         constructor_eager = pa.table
-    if library == "pyarrow" and PYARROW_VERSION < (13,):
-        pytest.skip()
-
     df = nw.from_native(constructor_eager({"values": data})).select(
         nw.col("values").cast(nw.Float64)
     )
diff --git a/tests/series_only/to_numpy_test.py b/tests/series_only/to_numpy_test.py
index eb2da85a..292faf72 100644
--- a/tests/series_only/to_numpy_test.py
+++ b/tests/series_only/to_numpy_test.py
@@ -8,7 +8,7 @@ import pytest
 from numpy.testing import assert_array_equal
 
 import narwhals as nw
-from tests.utils import PANDAS_VERSION, PYARROW_VERSION, is_windows
+from tests.utils import PANDAS_VERSION, is_windows
 
 if TYPE_CHECKING:
     from tests.utils import ConstructorEager
@@ -38,8 +38,7 @@ def test_to_numpy_tz_aware(
     constructor_eager: ConstructorEager, request: pytest.FixtureRequest
 ) -> None:
     if (
-        ("pyarrow_table" in str(constructor_eager) and PYARROW_VERSION < (12,))
-        or ("pandas_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2, 2))
+        ("pandas_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2, 2))
         or ("modin_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2, 2))
         or ("pyarrow" in str(constructor_eager) and is_windows())
     ):
diff --git a/tests/v1_test.py b/tests/v1_test.py
index 3ac17b45..e831dd3c 100644
--- a/tests/v1_test.py
+++ b/tests/v1_test.py
@@ -34,7 +34,6 @@ from tests.utils import (
     DUCKDB_VERSION,
     PANDAS_VERSION,
     POLARS_VERSION,
-    PYARROW_VERSION,
     Constructor,
     ConstructorEager,
     assert_equal_data,
@@ -207,8 +206,6 @@ def test_to_dict_as_series(constructor_eager: ConstructorEager) -> None:
     "ignore:`Series.hist` is being called from the stable API although considered an unstable feature."
 )
 def test_hist_v1(constructor_eager: ConstructorEager) -> None:
-    if "pyarrow_table" in str(constructor_eager) and PYARROW_VERSION < (13,):
-        pytest.skip()
     if "cudf" in str(constructor_eager):
         pytest.skip()
     df = nw_v1.from_native(constructor_eager({"a": [1, 1, 2]}), eager_only=True)