Repository: narwhals-dev/narwhals

Issue:
feat: add Series.to_arrow

Discussion:

Patch:
diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md
index 2479196a..7b7f62b8 100644
--- a/docs/api-reference/series.md
+++ b/docs/api-reference/series.md
@@ -47,6 +47,7 @@
         - std
         - sum
         - tail
+        - to_arrow
         - to_dummies
         - to_frame
         - to_list
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index fadc72b8..bce0f471 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -615,6 +615,9 @@ class ArrowSeries:
 
         return self._from_native_series(arr)
 
+    def to_arrow(self: Self) -> Any:
+        return self._native_series.combine_chunks()
+
     @property
     def shape(self) -> tuple[int]:
         return (len(self._native_series),)
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index e52a034e..9a80e26f 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -17,6 +17,7 @@ from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
+from narwhals.dependencies import get_pyarrow
 from narwhals.dependencies import get_pyarrow_compute
 from narwhals.utils import Implementation
 
@@ -638,6 +639,14 @@ class PandasLikeSeries:
             self._native_series.clip(lower_bound, upper_bound)
         )
 
+    def to_arrow(self: Self) -> Any:
+        if self._implementation is Implementation.CUDF:  # pragma: no cover
+            msg = "`to_arrow` is not implemented for CuDF backend."
+            raise NotImplementedError(msg)
+
+        pa = get_pyarrow()
+        return pa.Array.from_pandas(self._native_series)
+
     @property
     def str(self) -> PandasLikeSeriesStringNamespace:
         return PandasLikeSeriesStringNamespace(self)
diff --git a/narwhals/series.py b/narwhals/series.py
index 76dd71e6..a6351f20 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -2227,6 +2227,45 @@ class Series:
             self._compliant_series.gather_every(n=n, offset=offset)
         )
 
+    def to_arrow(self: Self) -> Any:
+        r"""
+        Convert to arrow.
+
+        Examples:
+            >>> import narwhals as nw
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> data = [1, 2, 3, 4]
+            >>> s_pd = pd.Series(name="a", data=data)
+            >>> s_pl = pl.Series(name="a", values=data)
+
+            Let's define a dataframe-agnostic function that converts to arrow:
+
+            >>> @nw.narwhalify
+            ... def func(s):
+            ...     return s.to_arrow()
+
+            >>> func(s_pd)  # doctest:+NORMALIZE_WHITESPACE
+            <pyarrow.lib.Int64Array object at ...>
+            [
+                1,
+                2,
+                3,
+                4
+            ]
+
+            >>> func(s_pl)  # doctest:+NORMALIZE_WHITESPACE
+            <pyarrow.lib.Int64Array object at ...>
+            [
+                1,
+                2,
+                3,
+                4
+            ]
+        """
+
+        return self._compliant_series.to_arrow()
+
     @property
     def str(self) -> SeriesStringNamespace:
         return SeriesStringNamespace(self)
diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py
index 80ee5d7a..68c98008 100644
--- a/utils/check_api_reference.py
+++ b/utils/check_api_reference.py
@@ -148,6 +148,7 @@ if (
     .difference(expr)
     .difference(
         {
+            "to_arrow",
             "to_dummies",
             "to_pandas",
             "to_list",


Test patch:
diff --git a/tests/series_only/to_arrow_test.py b/tests/series_only/to_arrow_test.py
new file mode 100644
index 00000000..ebd90b7c
--- /dev/null
+++ b/tests/series_only/to_arrow_test.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pyarrow as pa
+import pyarrow.compute as pc
+import pytest
+
+import narwhals.stable.v1 as nw
+
+
+def test_to_arrow(constructor_eager: Any) -> None:
+    data = [1, 2, 3]
+    result = nw.from_native(constructor_eager({"a": data}), eager_only=True)[
+        "a"
+    ].to_arrow()
+
+    assert pa.types.is_int64(result.type)
+    assert pc.all(pc.equal(result, pa.array(data, type=pa.int64())))
+
+
+def test_to_arrow_with_nulls(constructor_eager: Any, request: Any) -> None:
+    if "pandas_constructor" in str(constructor_eager) or "modin_constructor" in str(
+        constructor_eager
+    ):
+        request.applymarker(pytest.mark.xfail)
+
+    data = [1, 2, None]
+    result = (
+        nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"]
+        .cast(nw.Int64)
+        .to_arrow()
+    )
+
+    assert pa.types.is_int64(result.type)
+    assert pc.all(pc.equal(result, pa.array(data, type=pa.int64())))