Repository: narwhals-dev/narwhals (python)

Issue:
[Enh]: add support for `.to_polars()`
# Description 

We have `to_pandas`, `to_arrow`, `to_numpy`, all of which are supported in the polars API. Clearly polars does not need a `to_polars` method, but _maybe_ we could. I don't have a particular use case in mind as of now though, except for the interchange level support, which could decide which backend materialize the data into if needed.


Pull request description:
feat: add `DataFrame` and `Series` `to_polars`
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Closes #1318

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added
- [x] Documented the changes

## If you have comments or can explain your changes, please do so below


Patch:
diff --git a/docs/api-reference/dataframe.md b/docs/api-reference/dataframe.md
index d937bf28..9e12c389 100644
--- a/docs/api-reference/dataframe.md
+++ b/docs/api-reference/dataframe.md
@@ -44,6 +44,7 @@
         - to_native
         - to_numpy
         - to_pandas
+        - to_polars
         - unique
         - unpivot
         - with_columns
diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md
index 0aea494f..d3bbd62f 100644
--- a/docs/api-reference/series.md
+++ b/docs/api-reference/series.md
@@ -77,6 +77,7 @@
         - to_list
         - to_numpy
         - to_pandas
+        - to_polars
         - to_native
         - unique
         - value_counts
diff --git a/docs/installation.md b/docs/installation.md
index 8857b802..59a884ef 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -30,7 +30,7 @@ To verify the installation, start the Python REPL and execute:
 ```python
 >>> import narwhals
 >>> narwhals.__version__
-'1.21.1'
+'1.22.0'
 ```
 
 If you see the version number, then the installation was successful!
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index fedc35b3..a7078355 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -79,7 +79,7 @@ from narwhals.utils import maybe_get_index
 from narwhals.utils import maybe_reset_index
 from narwhals.utils import maybe_set_index
 
-__version__ = "1.21.1"
+__version__ = "1.22.0"
 
 __all__ = [
     "Array",
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 61688795..ed738647 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
 
     import numpy as np
     import pandas as pd
+    import polars as pl
     import pyarrow as pa
     from typing_extensions import Self
 
@@ -427,6 +428,11 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
     def to_pandas(self: Self) -> pd.DataFrame:
         return self._native_frame.to_pandas()
 
+    def to_polars(self: Self) -> pl.DataFrame:
+        import polars as pl  # ignore-banned-import
+
+        return pl.from_arrow(self._native_frame)  # type: ignore[return-value]
+
     def to_numpy(self: Self) -> np.ndarray:
         import numpy as np  # ignore-banned-import
 
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 656a802c..873b2997 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
 
     import numpy as np
     import pandas as pd
+    import polars as pl
     import pyarrow as pa
     from typing_extensions import Self
 
@@ -733,6 +734,11 @@ class ArrowSeries(CompliantSeries):
 
         return pd.Series(self._native_series, name=self.name)
 
+    def to_polars(self: Self) -> pl.Series:
+        import polars as pl  # ignore-banned-import
+
+        return pl.from_arrow(self._native_series)  # type: ignore[return-value]
+
     def is_duplicated(self: Self) -> ArrowSeries:
         return self.to_frame().is_duplicated().alias(self.name)
 
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 0cff4cb3..47c43a69 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -35,6 +35,7 @@ if TYPE_CHECKING:
 
     import numpy as np
     import pandas as pd
+    import polars as pl
     from typing_extensions import Self
 
     from narwhals._pandas_like.group_by import PandasLikeGroupBy
@@ -763,12 +764,27 @@ class PandasLikeDataFrame:
                 )
         return df.to_numpy(copy=copy)
 
-    def to_pandas(self) -> Any:
+    def to_pandas(self: Self) -> pd.DataFrame:
         if self._implementation is Implementation.PANDAS:
             return self._native_frame
-        if self._implementation is Implementation.MODIN:
+        elif self._implementation is Implementation.CUDF:  # pragma: no cover
+            return self._native_frame.to_pandas()
+        elif self._implementation is Implementation.MODIN:
             return self._native_frame._to_pandas()
-        return self._native_frame.to_pandas()  # pragma: no cover
+        msg = f"Unknown implementation: {self._implementation}"  # pragma: no cover
+        raise AssertionError(msg)
+
+    def to_polars(self: Self) -> pl.DataFrame:
+        import polars as pl  # ignore-banned-import
+
+        if self._implementation is Implementation.PANDAS:
+            return pl.from_pandas(self._native_frame)
+        elif self._implementation is Implementation.CUDF:  # pragma: no cover
+            return pl.from_pandas(self._native_frame.to_pandas())
+        elif self._implementation is Implementation.MODIN:
+            return pl.from_pandas(self._native_frame._to_pandas())
+        msg = f"Unknown implementation: {self._implementation}"  # pragma: no cover
+        raise AssertionError(msg)
 
     def write_parquet(self, file: Any) -> Any:
         self._native_frame.to_parquet(file)
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 139c9a3b..358041db 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -29,6 +29,8 @@ from narwhals.utils import validate_backend_version
 if TYPE_CHECKING:
     from types import ModuleType
 
+    import pandas as pd
+    import polars as pl
     from typing_extensions import Self
 
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
@@ -303,13 +305,13 @@ class PandasLikeSeries(CompliantSeries):
     def arg_min(self) -> int:
         ser = self._native_series
         if self._implementation is Implementation.PANDAS and self._backend_version < (1,):
-            return ser.values.argmin()  # type: ignore[no-any-return]
+            return ser.to_numpy().argmin()  # type: ignore[no-any-return]
         return ser.argmin()  # type: ignore[no-any-return]
 
     def arg_max(self) -> int:
         ser = self._native_series
         if self._implementation is Implementation.PANDAS and self._backend_version < (1,):
-            return ser.values.argmax()  # type: ignore[no-any-return]
+            return ser.to_numpy().argmax()  # type: ignore[no-any-return]
         return ser.argmax()  # type: ignore[no-any-return]
 
     # Binary comparisons
@@ -837,16 +839,28 @@ class PandasLikeSeries(CompliantSeries):
             )
         return s.to_numpy(dtype=dtype, copy=copy)
 
-    def to_pandas(self) -> Any:
+    def to_pandas(self: Self) -> pd.Series:
         if self._implementation is Implementation.PANDAS:
             return self._native_series
-        elif self._implementation is Implementation.CUDF:
+        elif self._implementation is Implementation.CUDF:  # pragma: no cover
             return self._native_series.to_pandas()
         elif self._implementation is Implementation.MODIN:
             return self._native_series._to_pandas()
         msg = f"Unknown implementation: {self._implementation}"  # pragma: no cover
         raise AssertionError(msg)
 
+    def to_polars(self: Self) -> pl.DataFrame:
+        import polars as pl  # ignore-banned-import
+
+        if self._implementation is Implementation.PANDAS:
+            return pl.from_pandas(self._native_series)
+        elif self._implementation is Implementation.CUDF:  # pragma: no cover
+            return pl.from_pandas(self._native_series.to_pandas())
+        elif self._implementation is Implementation.MODIN:
+            return pl.from_pandas(self._native_series._to_pandas())
+        msg = f"Unknown implementation: {self._implementation}"  # pragma: no cover
+        raise AssertionError(msg)
+
     # --- descriptive ---
     def is_duplicated(self: Self) -> Self:
         res = self._native_series.duplicated(keep=False)
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
index d5e11528..763a0981 100644
--- a/narwhals/_polars/dataframe.py
+++ b/narwhals/_polars/dataframe.py
@@ -332,6 +332,9 @@ class PolarsDataFrame:
         )
         return self._from_native_object(result)
 
+    def to_polars(self: Self) -> pl.DataFrame:
+        return self._native_frame
+
 
 class PolarsLazyFrame:
     def __init__(
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index 33572db7..32289632 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -417,6 +417,9 @@ class PolarsSeries:
             msg = f"Unable to compare other of type {type(other)} with series of type {self.dtype}."
             raise InvalidOperationError(msg) from exc
 
+    def to_polars(self: Self) -> pl.Series:
+        return self._native_series
+
     @property
     def dt(self: Self) -> PolarsSeriesDateTimeNamespace:
         return PolarsSeriesDateTimeNamespace(self)
diff --git a/narwhals/_spark_like/namespace.py b/narwhals/_spark_like/namespace.py
index f53f66f7..a8c778b8 100644
--- a/narwhals/_spark_like/namespace.py
+++ b/narwhals/_spark_like/namespace.py
@@ -3,10 +3,12 @@ from __future__ import annotations
 import operator
 from functools import reduce
 from typing import TYPE_CHECKING
+from typing import Any
 from typing import Iterable
 from typing import Literal
 
 from narwhals._expression_parsing import combine_root_names
+from narwhals._expression_parsing import parse_into_expr
 from narwhals._expression_parsing import parse_into_exprs
 from narwhals._expression_parsing import reduce_output_names
 from narwhals._spark_like.dataframe import SparkLikeLazyFrame
@@ -334,3 +336,103 @@ class SparkLikeNamespace(CompliantNamespace["Column"]):
                 "ignore_nulls": ignore_nulls,
             },
         )
+
+    def when(self, *predicates: IntoSparkLikeExpr) -> SparkLikeWhen:
+        plx = self.__class__(backend_version=self._backend_version, version=self._version)
+        condition = plx.all_horizontal(*predicates)
+        return SparkLikeWhen(
+            condition, self._backend_version, returns_scalar=False, version=self._version
+        )
+
+
+class SparkLikeWhen:
+    def __init__(
+        self,
+        condition: SparkLikeExpr,
+        backend_version: tuple[int, ...],
+        then_value: Any | None = None,
+        otherwise_value: Any | None = None,
+        *,
+        returns_scalar: bool,
+        version: Version,
+    ) -> None:
+        self._backend_version = backend_version
+        self._condition = condition
+        self._then_value = then_value
+        self._otherwise_value = otherwise_value
+        self._returns_scalar = returns_scalar
+        self._version = version
+
+    def __call__(self, df: SparkLikeLazyFrame) -> list[Column]:
+        from pyspark.sql import functions as F  # noqa: N812
+
+        plx = df.__narwhals_namespace__()
+        condition = parse_into_expr(self._condition, namespace=plx)(df)[0]
+
+        try:
+            value_ = parse_into_expr(self._then_value, namespace=plx)(df)[0]
+            col_name = get_column_name(df, value_)
+        except TypeError:
+            # `self._then_value` is a scalar and can't be converted to an expression
+            value_ = F.lit(self._then_value)
+            col_name = "literal"
+
+        try:
+            other_ = parse_into_expr(self._otherwise_value, namespace=plx)(df)[0]
+        except TypeError:
+            # `self._otherwise_value` is a scalar and can't be converted to an expression
+            other_ = F.lit(self._otherwise_value)
+
+        return [
+            F.when(condition=condition, value=value_)
+            .otherwise(value=other_)
+            .alias(col_name)
+        ]
+
+    def then(self, value: SparkLikeExpr | Any) -> SparkLikeThen:
+        self._then_value = value
+
+        return SparkLikeThen(  # type: ignore[abstract]
+            self,
+            depth=0,
+            function_name="whenthen",
+            root_names=None,
+            output_names=None,
+            returns_scalar=self._returns_scalar,
+            backend_version=self._backend_version,
+            version=self._version,
+            kwargs={"value": value},
+        )
+
+
+class SparkLikeThen(SparkLikeExpr):
+    def __init__(
+        self,
+        call: SparkLikeWhen,
+        *,
+        depth: int,
+        function_name: str,
+        root_names: list[str] | None,
+        output_names: list[str] | None,
+        returns_scalar: bool,
+        backend_version: tuple[int, ...],
+        version: Version,
+        kwargs: dict[str, Any],
+    ) -> None:
+        self._backend_version = backend_version
+        self._version = version
+        self._call = call
+        self._depth = depth
+        self._function_name = function_name
+        self._root_names = root_names
+        self._output_names = output_names
+        self._returns_scalar = returns_scalar
+        self._kwargs = kwargs
+
+    def otherwise(self, value: SparkLikeExpr | Any) -> SparkLikeExpr:
+        # type ignore because we are setting the `_call` attribute to a
+        # callable object of type `SparkLikeWhen`, base class has the attribute as
+        # only a `Callable`
+        self._call._otherwise_value = value  # type: ignore[attr-defined]
+        self._function_name = "whenotherwise"
+        return self
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index dd786ef3..1ae43028 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
 
     import numpy as np
     import pandas as pd
+    import polars as pl
     import pyarrow as pa
     from typing_extensions import Self
 
@@ -581,6 +582,72 @@ class DataFrame(BaseFrame[DataFrameT]):
         """
         return self._compliant_frame.to_pandas()
 
+    def to_polars(self) -> pl.DataFrame:
+        """Convert this DataFrame to a polars DataFrame.
+
+        Returns:
+            A polars DataFrame.
+
+        Examples:
+            Construct pandas, Polars (eager) and PyArrow DataFrames:
+
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import pyarrow as pa
+            >>> import narwhals as nw
+            >>> from narwhals.typing import IntoDataFrame
+            >>> data = {"foo": [1, 2, 3], "bar": [6.0, 7.0, 8.0], "ham": ["a", "b", "c"]}
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.DataFrame(data)
+            >>> df_pa = pa.table(data)
+
+            We define a library agnostic function:
+
+            >>> def agnostic_to_polars(df_native: IntoDataFrame) -> pl.DataFrame:
+            ...     df = nw.from_native(df_native)
+            ...     return df.to_polars()
+
+            We can then pass any supported library such as pandas, Polars (eager), or
+            PyArrow to `agnostic_to_polars`:
+
+            >>> agnostic_to_polars(df_pd)
+            shape: (3, 3)
+            ┌─────┬─────┬─────┐
+            │ foo ┆ bar ┆ ham │
+            │ --- ┆ --- ┆ --- │
+            │ i64 ┆ f64 ┆ str │
+            ╞═════╪═════╪═════╡
+            │ 1   ┆ 6.0 ┆ a   │
+            │ 2   ┆ 7.0 ┆ b   │
+            │ 3   ┆ 8.0 ┆ c   │
+            └─────┴─────┴─────┘
+
+            >>> agnostic_to_polars(df_pl)
+            shape: (3, 3)
+            ┌─────┬─────┬─────┐
+            │ foo ┆ bar ┆ ham │
+            │ --- ┆ --- ┆ --- │
+            │ i64 ┆ f64 ┆ str │
+            ╞═════╪═════╪═════╡
+            │ 1   ┆ 6.0 ┆ a   │
+            │ 2   ┆ 7.0 ┆ b   │
+            │ 3   ┆ 8.0 ┆ c   │
+            └─────┴─────┴─────┘
+
+            >>> agnostic_to_polars(df_pa)
+            shape: (3, 3)
+            ┌─────┬─────┬─────┐
+            │ foo ┆ bar ┆ ham │
+            │ --- ┆ --- ┆ --- │
+            │ i64 ┆ f64 ┆ str │
+            ╞═════╪═════╪═════╡
+            │ 1   ┆ 6.0 ┆ a   │
+            │ 2   ┆ 7.0 ┆ b   │
+            │ 3   ┆ 8.0 ┆ c   │
+            └─────┴─────┴─────┘
+        """
+        return self._compliant_frame.to_polars()  # type: ignore[no-any-return]
+
     @overload
     def write_csv(self, file: None = None) -> str: ...
 
diff --git a/narwhals/series.py b/narwhals/series.py
index 46ed53ab..dc80056c 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -26,6 +26,7 @@ if TYPE_CHECKING:
 
     import numpy as np
     import pandas as pd
+    import polars as pl
     import pyarrow as pa
     from typing_extensions import Self
 
@@ -2764,8 +2765,8 @@ class Series(Generic[IntoSeriesT]):
         """
         return self._compliant_series.to_numpy()
 
-    def to_pandas(self) -> pd.Series:
-        """Convert to pandas.
+    def to_pandas(self: Self) -> pd.Series:
+        """Convert to pandas Series.
 
         Returns:
             A pandas Series containing the data from this Series.
@@ -2811,6 +2812,62 @@ class Series(Generic[IntoSeriesT]):
         """
         return self._compliant_series.to_pandas()
 
+    def to_polars(self: Self) -> pl.Series:
+        """Convert to polars Series.
+
+        Returns:
+            A polars Series containing the data from this Series.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import pyarrow as pa
+            >>> import narwhals as nw
+            >>> from narwhals.typing import IntoSeries
+
+            >>> data = [1, 2, 3]
+            >>> s_pd = pd.Series(data, name="a")
+            >>> s_pl = pl.Series("a", data)
+            >>> s_pa = pa.chunked_array([data])
+
+            We define a library agnostic function:
+
+            >>> def agnostic_to_polars(s_native: IntoSeries) -> pd.Series:
+            ...     s = nw.from_native(s_native, series_only=True)
+            ...     return s.to_polars()
+
+            We can then pass any supported library such as pandas, Polars, or
+            PyArrow to `agnostic_to_polars`:
+
+            >>> agnostic_to_polars(s_pd)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (3,)
+            Series: 'a' [i64]
+            [
+                1
+                2
+                3
+            ]
+
+            >>> agnostic_to_polars(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (3,)
+            Series: 'a' [i64]
+            [
+                1
+                2
+                3
+            ]
+
+            >>> agnostic_to_polars(s_pa)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (3,)
+            Series: '' [i64]
+            [
+                1
+                2
+                3
+            ]
+        """
+        return self._compliant_series.to_polars()  # type: ignore[no-any-return]
+
     def __add__(self, other: object) -> Self:
         return self._from_compliant_series(
             self._compliant_series.__add__(self._extract_native(other))
diff --git a/pyproject.toml b/pyproject.toml
index 37dd3696..c461d703 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "narwhals"
-version = "1.21.1"
+version = "1.22.0"
 dependencies = []
 requires-python = ">=3.8"
 authors = [
diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py
index 2b85a2dc..1b186774 100644
--- a/utils/check_api_reference.py
+++ b/utils/check_api_reference.py
@@ -30,6 +30,7 @@ SERIES_ONLY_METHODS = {
     "to_native",
     "to_numpy",
     "to_pandas",
+    "to_polars",
     "value_counts",
     "zip_with",
     "__iter__",


Test patch:
diff --git a/tests/expr_and_series/when_test.py b/tests/expr_and_series/when_test.py
index 0faf5917..94e37aaa 100644
--- a/tests/expr_and_series/when_test.py
+++ b/tests/expr_and_series/when_test.py
@@ -17,9 +17,7 @@ data = {
 }
 
 
-def test_when(constructor: Constructor, request: pytest.FixtureRequest) -> None:
-    if "pyspark" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_when(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") == 1).then(value=3).alias("a_when"))
     expected = {
@@ -28,9 +26,7 @@ def test_when(constructor: Constructor, request: pytest.FixtureRequest) -> None:
     assert_equal_data(result, expected)
 
 
-def test_when_otherwise(constructor: Constructor, request: pytest.FixtureRequest) -> None:
-    if "pyspark" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_when_otherwise(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") == 1).then(3).otherwise(6).alias("a_when"))
     expected = {
@@ -39,11 +35,7 @@ def test_when_otherwise(constructor: Constructor, request: pytest.FixtureRequest
     assert_equal_data(result, expected)
 
 
-def test_multiple_conditions(
-    constructor: Constructor, request: pytest.FixtureRequest
-) -> None:
-    if "pyspark" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_multiple_conditions(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(
         nw.when(nw.col("a") < 3, nw.col("c") < 5.0).then(3).alias("a_when")
@@ -85,11 +77,7 @@ def test_value_series(constructor_eager: ConstructorEager) -> None:
     assert_equal_data(result, expected)
 
 
-def test_value_expression(
-    constructor: Constructor, request: pytest.FixtureRequest
-) -> None:
-    if "pyspark" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_value_expression(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") == 1).then(nw.col("a") + 9).alias("a_when"))
     expected = {
@@ -122,11 +110,7 @@ def test_otherwise_series(constructor_eager: ConstructorEager) -> None:
     assert_equal_data(result, expected)
 
 
-def test_otherwise_expression(
-    constructor: Constructor, request: pytest.FixtureRequest
-) -> None:
-    if "pyspark" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_otherwise_expression(constructor: Constructor) -> None:
     df = nw.from_native(constructor(data))
     result = df.select(
         nw.when(nw.col("a") == 1).then(-1).otherwise(nw.col("a") + 7).alias("a_when")
@@ -140,7 +124,7 @@ def test_otherwise_expression(
 def test_when_then_otherwise_into_expr(
     constructor: Constructor, request: pytest.FixtureRequest
 ) -> None:
-    if ("pyspark" in str(constructor)) or "duckdb" in str(constructor):
+    if "duckdb" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") > 1).then("c").otherwise("e"))
@@ -151,7 +135,7 @@ def test_when_then_otherwise_into_expr(
 def test_when_then_otherwise_lit_str(
     constructor: Constructor, request: pytest.FixtureRequest
 ) -> None:
-    if ("pyspark" in str(constructor)) or "duckdb" in str(constructor):
+    if "duckdb" in str(constructor):
         request.applymarker(pytest.mark.xfail)
     df = nw.from_native(constructor(data))
     result = df.select(nw.when(nw.col("a") > 1).then(nw.col("b")).otherwise(nw.lit("z")))
diff --git a/tests/frame/to_polars_test.py b/tests/frame/to_polars_test.py
new file mode 100644
index 00000000..d8683cbd
--- /dev/null
+++ b/tests/frame/to_polars_test.py
@@ -0,0 +1,23 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import polars as pl
+import pytest
+from polars.testing import assert_frame_equal
+
+import narwhals.stable.v1 as nw
+
+if TYPE_CHECKING:
+    from tests.utils import ConstructorEager
+
+
+@pytest.mark.filterwarnings("ignore:.*Passing a BlockManager.*:DeprecationWarning")
+def test_convert_polars(constructor_eager: ConstructorEager) -> None:
+    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.1, 8, 9]}
+    df_raw = constructor_eager(data)
+    result = nw.from_native(df_raw).to_polars()  # type: ignore[union-attr]
+
+    expected = pl.DataFrame(data)
+
+    assert_frame_equal(result, expected)
diff --git a/tests/series_only/to_polars_test.py b/tests/series_only/to_polars_test.py
new file mode 100644
index 00000000..b5e66ea8
--- /dev/null
+++ b/tests/series_only/to_polars_test.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import polars as pl
+from polars.testing import assert_series_equal
+
+import narwhals.stable.v1 as nw
+
+if TYPE_CHECKING:
+    from tests.utils import ConstructorEager
+
+data = [1, 3, 2]
+
+
+def test_series_to_polars(constructor_eager: ConstructorEager) -> None:
+    result = (
+        nw.from_native(constructor_eager({"a": data}), eager_only=True)["a"]
+        .alias("a")
+        .to_polars()
+    )
+
+    expected = pl.Series("a", data)
+    assert_series_equal(result, expected)