Repository: narwhals-dev/narwhals

Issue:
Implement Series.to_native
Currently, we can call `nw.to_native`

It would be more user-friendly to have `Series.to_native` though, I keep finding myself reaching for this

Contributing guide: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md

Discussion:

Patch:
diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md
index c016b566..9868e7b9 100644
--- a/docs/api-reference/series.md
+++ b/docs/api-reference/series.md
@@ -57,6 +57,7 @@
         - to_list
         - to_numpy
         - to_pandas
+        - to_native
         - unique
         - value_counts
         - zip_with
diff --git a/docs/extending.md b/docs/extending.md
index 13b93387..f6829ba3 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -4,11 +4,13 @@
 
 Currently, Narwhals supports the following libraries as inputs:
 
-- pandas
-- Polars
-- cuDF
-- Modin
-- PyArrow
+| Library  | 🔗 Link 🔗 |
+| ------------- | ------------- |
+| ️Polars 🐻‍❄️ | [github.com/pola-rs/polars](https://github.com/pola-rs/polars) |
+| pandas 🐼 |  [github.com/pandas-dev/pandas](https://github.com/pandas-dev/pandas) |
+| cuDF | [github.com/rapidsai/cudf](https://github.com/rapidsai/cudf) |
+| Modin | [github.com/modin-project/modin](https://github.com/modin-project/modin) |
+| PyArrow ⇶ | [arrow.apache.org/docs/python](https://arrow.apache.org/docs/python/index.html) |
 
 If you want your own library to be recognised too, you're welcome open a PR (with tests)!
 Alternatively, if you can't do that (for example, if you library is closed-source), see
diff --git a/docs/installation.md b/docs/installation.md
index ac7d2341..2f801254 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -28,6 +28,7 @@ they are not required dependencies - Narwhals only ever uses what the user passe
 
 - [pandas](https://pandas.pydata.org/docs/getting_started/install.html)
 - [Polars](https://pola-rs.github.io/polars/user-guide/installation/)
+- [PyArrow](https://arrow.apache.org/docs/python/install.html)
 
 ### Simple example
 
@@ -38,6 +39,7 @@ from __future__ import annotations
 
 import pandas as pd
 import polars as pl
+import pyarrow as pa
 import narwhals as nw
 from narwhals.typing import IntoFrame
 
@@ -48,13 +50,19 @@ def my_function(df_native: IntoFrame) -> list[str]:
     return column_names
 
 
-df_pandas = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
-df_polars = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+data = {"a": [1, 2, 3], "b": [4, 5, 6]}
+df_pandas = pd.DataFrame(data)
+df_polars = pl.DataFrame(data)
+table_pa = pa.table(data)
 
 print("pandas output")
 print(my_function(df_pandas))
+
 print("Polars output")
 print(my_function(df_polars))
+
+print("PyArrow output")
+print(my_function(table_pa))
 ```
 
 If you run `python t.py` then your output should look like the above. This is the simplest possible example of a dataframe-agnostic
diff --git a/docs/roadmap_and_related.md b/docs/roadmap_and_related.md
index 5e464683..43580db6 100644
--- a/docs/roadmap_and_related.md
+++ b/docs/roadmap_and_related.md
@@ -23,3 +23,9 @@ this includes Ibis and Vaex. See [extending](extending.md) for details.
 ### Array API
 
 Array counterpart to the DataFrame API, see [here](https://data-apis.org/array-api/2022.12/index.html).
+
+### PyCapsule Interface
+
+Allows C extension modules to safely share pointers to C data structures with Python code and other C modules, encapsulating the pointer with a name and optional destructor to manage resources and ensure safe access, see [here](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html) for details. 
+
+Narwhals supports exporting a DataFrame via the Arrow PyCapsule Interface.
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 16b869ff..7ac21698 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -395,14 +395,16 @@ class DataFrame(BaseFrame[FrameT]):
         Convert this DataFrame to a pandas DataFrame.
 
         Examples:
-            Construct pandas and Polars DataFrames:
+            Construct pandas, Polars (eager) and PyArrow DataFrames:
 
             >>> import pandas as pd
             >>> import polars as pl
+            >>> import pyarrow as pa
             >>> import narwhals as nw
             >>> df = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]}
             >>> df_pd = pd.DataFrame(df)
             >>> df_pl = pl.DataFrame(df)
+            >>> df_pa = pa.table(df)
 
             We define a library agnostic function:
 
@@ -410,7 +412,7 @@ class DataFrame(BaseFrame[FrameT]):
             ... def func(df):
             ...     return df.to_pandas()
 
-            We can then pass either pandas or Polars to `func`:
+            We can then pass any supported library such as pandas, Polars (eager), or PyArrow to `func`:
 
             >>> func(df_pd)
                foo  bar ham
@@ -422,6 +424,13 @@ class DataFrame(BaseFrame[FrameT]):
             0    1  6.0   a
             1    2  7.0   b
             2    3  8.0   c
+            >>> func(df_pa)
+               foo  bar ham
+            0    1  6.0   a
+            1    2  7.0   b
+            2    3  8.0   c
+
+
         """
         return self._compliant_frame.to_pandas()
 
diff --git a/narwhals/series.py b/narwhals/series.py
index 9d21058f..f34217c9 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -22,7 +22,7 @@ class Series:
     """
     Narwhals Series, backed by a native series.
 
-    The native dataframe might be pandas.Series, polars.Series, ...
+    The native series might be pandas.Series, polars.Series, ...
 
     This class is not meant to be instantiated directly - instead, use
     `narwhals.from_native`, making sure to pass `allow_series=True` or
@@ -85,6 +85,45 @@ class Series:
         ca = pa.chunked_array([self.to_arrow()])
         return ca.__arrow_c_stream__(requested_schema=requested_schema)
 
+    def to_native(self) -> Any:
+        """
+        Convert Narwhals series to native series.
+
+        Returns:
+            Series of class that user started with.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import narwhals as nw
+            >>> s = [1, 2, 3]
+            >>> s_pd = pd.Series(s)
+            >>> s_pl = pl.Series(s)
+
+            We define a library agnostic function:
+
+            >>> @nw.narwhalify
+            ... def func(s):
+            ...     return s.to_native()
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(s_pd)
+            0    1
+            1    2
+            2    3
+            dtype: int64
+            >>> func(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (3,)
+            Series: '' [i64]
+            [
+                1
+                2
+                3
+            ]
+        """
+        return self._compliant_series._native_series
+
     def scatter(self, indices: int | Sequence[int], values: Any) -> Self:
         """
         Set value(s) at given position(s).
@@ -224,7 +263,7 @@ class Series:
             + "─" * length
             + "┐\n"
             + f"|{header}|\n"
-            + "| Use `narwhals.to_native()` to see native output |\n"
+            + "| Use `series.to_native()` to see native output |\n"
             + "└"
             + "─" * length
             + "┘"
diff --git a/narwhals/stable/v1.py b/narwhals/stable/v1.py
index 1f103aae..1cd73125 100644
--- a/narwhals/stable/v1.py
+++ b/narwhals/stable/v1.py
@@ -342,7 +342,7 @@ class Series(NwSeries):
     """
     Narwhals Series, backed by a native series.
 
-    The native dataframe might be pandas.Series, polars.Series, ...
+    The native series might be pandas.Series, polars.Series, ...
 
     This class is not meant to be instantiated directly - instead, use
     `narwhals.from_native`, making sure to pass `allow_series=True` or
diff --git a/pyproject.toml b/pyproject.toml
index 0196124f..f2109e11 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -99,6 +99,7 @@ force-single-line = true
 docstring-code-format = true
 
 [tool.pytest.ini_options]
+testpaths = ["tests"]
 filterwarnings = [
   "error",
   'ignore:distutils Version classes are deprecated:DeprecationWarning',
diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py
index 1bf1f086..0f95dfed 100644
--- a/utils/check_api_reference.py
+++ b/utils/check_api_reference.py
@@ -27,6 +27,7 @@ SERIES_ONLY_METHODS = {
     "zip_with",
     "item",
     "scatter",
+    "to_native",
 }
 
 # TODO(Unassigned): make dtypes reference page as well


Test patch:
diff --git a/tests/series_only/to_native_test.py b/tests/series_only/to_native_test.py
new file mode 100644
index 00000000..269348ea
--- /dev/null
+++ b/tests/series_only/to_native_test.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from typing import Any
+
+import narwhals.stable.v1 as nw
+
+data = [4, 4, 4, 1, 6, 6, 4, 4, 1, 1]
+
+
+def test_to_native(constructor_eager: Any) -> None:
+    orig_series = constructor_eager({"a": data})["a"]
+    nw_series = nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"]
+    result = nw_series.to_native()
+    assert isinstance(result, orig_series.__class__)