Repository: narwhals-dev/narwhals
Pull request #1975 (2025-02-08 23:27:47)

chore: widen `parse_version` to accept packages
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [x] 🐳 Other

## Related issues
- Related issue #1942


## 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
- Little bit of #1942
- When combined with another change (https://github.com/narwhals-dev/narwhals/discussions/1657#discussioncomment-12054784), a lot of cases might become one-liners
- Generally, just reducing the amount of repetition



dangotbanned: ### Throwaway idea
If every backend accepted an `Implementation`, you could move call `parse_version` on it as a method.
Would save another parameter in the constructor (e,g, `PandasLikeDataFrame`).
I assume both `backend_version` & `implementation` are derived from the same package.
dangotbanned: > [!IMPORTANT]
> I'm not fully convinced this idea is beneficial enough alone 
dangotbanned: > thanks @dangotbanned , seems like a nice cleanup

Oh great, thanks @MarcoGorelli 

Patch:
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 50bb7bf2..e2c52c96 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -555,7 +555,7 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
             df = self._native_frame  # noqa: F841
             return DuckDBLazyFrame(
                 df=duckdb.table("df"),
-                backend_version=parse_version(duckdb.__version__),
+                backend_version=parse_version(duckdb),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -566,7 +566,7 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
             return PolarsLazyFrame(
                 df=pl.from_arrow(self._native_frame).lazy(),  # type: ignore[union-attr]
-                backend_version=parse_version(pl.__version__),
+                backend_version=parse_version(pl),
                 version=self._version,
             )
         elif backend is Implementation.DASK:
@@ -577,7 +577,7 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
             return DaskLazyFrame(
                 native_dataframe=dd.from_pandas(self._native_frame.to_pandas()),
-                backend_version=parse_version(dask.__version__),
+                backend_version=parse_version(dask),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -606,7 +606,7 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
             return PandasLikeDataFrame(
                 native_dataframe=self._native_frame.to_pandas(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd.__version__),
+                backend_version=parse_version(pd),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -618,7 +618,7 @@ class ArrowDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
             return PolarsDataFrame(
                 df=pl.from_arrow(self._native_frame),  # type: ignore[arg-type]
-                backend_version=parse_version(pl.__version__),
+                backend_version=parse_version(pl),
                 version=self._version,
             )
 
diff --git a/narwhals/_dask/dataframe.py b/narwhals/_dask/dataframe.py
index 5cd9ff6f..6be77e60 100644
--- a/narwhals/_dask/dataframe.py
+++ b/narwhals/_dask/dataframe.py
@@ -106,7 +106,7 @@ class DaskLazyFrame(CompliantLazyFrame):
             return PandasLikeDataFrame(
                 result,
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd.__version__),
+                backend_version=parse_version(pd),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -118,7 +118,7 @@ class DaskLazyFrame(CompliantLazyFrame):
 
             return PolarsDataFrame(
                 pl.from_pandas(result),
-                backend_version=parse_version(pl.__version__),
+                backend_version=parse_version(pl),
                 version=self._version,
             )
 
@@ -129,7 +129,7 @@ class DaskLazyFrame(CompliantLazyFrame):
 
             return ArrowDataFrame(
                 pa.Table.from_pandas(result),
-                backend_version=parse_version(pa.__version__),
+                backend_version=parse_version(pa),
                 version=self._version,
                 validate_column_names=False,
             )
diff --git a/narwhals/_dask/utils.py b/narwhals/_dask/utils.py
index da247b78..20a1c9cf 100644
--- a/narwhals/_dask/utils.py
+++ b/narwhals/_dask/utils.py
@@ -129,9 +129,7 @@ def narwhals_to_native_dtype(dtype: DType | type[DType], version: Version) -> An
     if isinstance_or_issubclass(dtype, dtypes.UInt8):
         return "uint8"
     if isinstance_or_issubclass(dtype, dtypes.String):
-        if (pd := get_pandas()) is not None and parse_version(
-            pd.__version__
-        ) >= parse_version("2.0.0"):
+        if (pd := get_pandas()) is not None and parse_version(pd) >= (2, 0, 0):
             if get_pyarrow() is not None:
                 return "string[pyarrow]"
             return "string[python]"  # pragma: no cover
diff --git a/narwhals/_duckdb/dataframe.py b/narwhals/_duckdb/dataframe.py
index 69d9c8c1..53a8a676 100644
--- a/narwhals/_duckdb/dataframe.py
+++ b/narwhals/_duckdb/dataframe.py
@@ -100,7 +100,7 @@ class DuckDBLazyFrame(CompliantLazyFrame):
 
             return ArrowDataFrame(
                 native_dataframe=self._native_frame.arrow(),
-                backend_version=parse_version(pa.__version__),
+                backend_version=parse_version(pa),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -113,7 +113,7 @@ class DuckDBLazyFrame(CompliantLazyFrame):
             return PandasLikeDataFrame(
                 native_dataframe=self._native_frame.df(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd.__version__),
+                backend_version=parse_version(pd),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -125,7 +125,7 @@ class DuckDBLazyFrame(CompliantLazyFrame):
 
             return PolarsDataFrame(
                 df=self._native_frame.pl(),
-                backend_version=parse_version(pl.__version__),
+                backend_version=parse_version(pl),
                 version=self._version,
             )
 
@@ -257,7 +257,7 @@ class DuckDBLazyFrame(CompliantLazyFrame):
         # only if version is v1, keep around for backcompat
         import pandas as pd  # ignore-banned-import()
 
-        if parse_version(pd.__version__) >= parse_version("1.0.0"):
+        if parse_version(pd) >= (1, 0, 0):
             return self._native_frame.df()
         else:  # pragma: no cover
             msg = f"Conversion to pandas requires pandas>=1.0.0, found {pd.__version__}"
diff --git a/narwhals/_interchange/dataframe.py b/narwhals/_interchange/dataframe.py
index 947cfa26..ca3e7d59 100644
--- a/narwhals/_interchange/dataframe.py
+++ b/narwhals/_interchange/dataframe.py
@@ -115,7 +115,7 @@ class InterchangeFrame:
     def to_pandas(self: Self) -> pd.DataFrame:
         import pandas as pd  # ignore-banned-import()
 
-        if parse_version(pd.__version__) >= parse_version("1.5.0"):
+        if parse_version(pd) >= (1, 5, 0):
             return pd.api.interchange.from_dataframe(self._interchange_frame)
         else:  # pragma: no cover
             msg = (
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index d0d3915a..e9201fdc 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -565,7 +565,7 @@ class PandasLikeDataFrame(CompliantDataFrame, CompliantLazyFrame):
             return PandasLikeDataFrame(
                 self.to_pandas(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd.__version__),
+                backend_version=parse_version(pd),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -577,7 +577,7 @@ class PandasLikeDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
             return ArrowDataFrame(
                 native_dataframe=self.to_arrow(),
-                backend_version=parse_version(pa.__version__),
+                backend_version=parse_version(pa),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -589,7 +589,7 @@ class PandasLikeDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
             return PolarsDataFrame(
                 df=self.to_polars(),
-                backend_version=parse_version(pl.__version__),
+                backend_version=parse_version(pl),
                 version=self._version,
             )
 
@@ -810,7 +810,7 @@ class PandasLikeDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
             return DuckDBLazyFrame(
                 df=duckdb.table("pandas_df"),
-                backend_version=parse_version(duckdb.__version__),
+                backend_version=parse_version(duckdb),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -821,7 +821,7 @@ class PandasLikeDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
             return PolarsLazyFrame(
                 df=pl.from_pandas(pandas_df).lazy(),
-                backend_version=parse_version(pl.__version__),
+                backend_version=parse_version(pl),
                 version=self._version,
             )
         elif backend is Implementation.DASK:
@@ -832,7 +832,7 @@ class PandasLikeDataFrame(CompliantDataFrame, CompliantLazyFrame):
 
             return DaskLazyFrame(
                 native_dataframe=dd.from_pandas(pandas_df),
-                backend_version=parse_version(dask.__version__),
+                backend_version=parse_version(dask),
                 version=self._version,
                 validate_column_names=False,
             )
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
index 7692da5b..94472bc1 100644
--- a/narwhals/_polars/dataframe.py
+++ b/narwhals/_polars/dataframe.py
@@ -250,7 +250,7 @@ class PolarsDataFrame:
             df = self._native_frame  # noqa: F841
             return DuckDBLazyFrame(
                 df=duckdb.table("df"),
-                backend_version=parse_version(duckdb.__version__),
+                backend_version=parse_version(duckdb),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -262,7 +262,7 @@ class PolarsDataFrame:
 
             return DaskLazyFrame(
                 native_dataframe=dd.from_pandas(self._native_frame.to_pandas()),
-                backend_version=parse_version(dask.__version__),
+                backend_version=parse_version(dask),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -470,7 +470,7 @@ class PolarsLazyFrame:
             return PandasLikeDataFrame(
                 result.to_pandas(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd.__version__),
+                backend_version=parse_version(pd),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -482,7 +482,7 @@ class PolarsLazyFrame:
 
             return ArrowDataFrame(
                 result.to_arrow(),
-                backend_version=parse_version(pa.__version__),
+                backend_version=parse_version(pa),
                 version=self._version,
                 validate_column_names=False,
             )
diff --git a/narwhals/_spark_like/dataframe.py b/narwhals/_spark_like/dataframe.py
index c1e21058..7275687f 100644
--- a/narwhals/_spark_like/dataframe.py
+++ b/narwhals/_spark_like/dataframe.py
@@ -125,7 +125,7 @@ class SparkLikeLazyFrame(CompliantLazyFrame):
             return PandasLikeDataFrame(
                 native_dataframe=self._native_frame.toPandas(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd.__version__),
+                backend_version=parse_version(pd),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -159,7 +159,7 @@ class SparkLikeLazyFrame(CompliantLazyFrame):
                     raise
             return ArrowDataFrame(
                 native_pyarrow_frame,
-                backend_version=parse_version(pa.__version__),
+                backend_version=parse_version(pa),
                 version=self._version,
                 validate_column_names=False,
             )
@@ -174,7 +174,7 @@ class SparkLikeLazyFrame(CompliantLazyFrame):
                 df=pl.from_arrow(  # type: ignore[arg-type]
                     pa.Table.from_batches(self._native_frame._collect_as_arrow())
                 ),
-                backend_version=parse_version(pl.__version__),
+                backend_version=parse_version(pl),
                 version=self._version,
             )
 
diff --git a/narwhals/_spark_like/expr.py b/narwhals/_spark_like/expr.py
index ea5c07c5..50624f42 100644
--- a/narwhals/_spark_like/expr.py
+++ b/narwhals/_spark_like/expr.py
@@ -350,7 +350,7 @@ class SparkLikeExpr(CompliantExpr["Column"]):
         def _median(_input: Column) -> Column:
             import pyspark  # ignore-banned-import
 
-            if parse_version(pyspark.__version__) < (3, 4):
+            if parse_version(pyspark) < (3, 4):
                 # Use percentile_approx with default accuracy parameter (10000)
                 return self._F.percentile_approx(_input.cast("double"), 0.5)
 
@@ -377,12 +377,7 @@ class SparkLikeExpr(CompliantExpr["Column"]):
 
         from narwhals._spark_like.utils import _std
 
-        func = partial(
-            _std,
-            ddof=ddof,
-            np_version=parse_version(np.__version__),
-            functions=self._F,
-        )
+        func = partial(_std, ddof=ddof, np_version=parse_version(np), functions=self._F)
 
         return self._from_call(func, "std", expr_kind=ExprKind.AGGREGATION)
 
@@ -393,12 +388,7 @@ class SparkLikeExpr(CompliantExpr["Column"]):
 
         from narwhals._spark_like.utils import _var
 
-        func = partial(
-            _var,
-            ddof=ddof,
-            np_version=parse_version(np.__version__),
-            functions=self._F,
-        )
+        func = partial(_var, ddof=ddof, np_version=parse_version(np), functions=self._F)
 
         return self._from_call(func, "var", expr_kind=ExprKind.AGGREGATION)
 
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 81bdee55..19714128 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -500,7 +500,7 @@ class DataFrame(BaseFrame[DataFrameT]):
         except ModuleNotFoundError as exc:  # pragma: no cover
             msg = f"PyArrow>=14.0.0 is required for `DataFrame.__arrow_c_stream__` for object of type {type(native_frame)}"
             raise ModuleNotFoundError(msg) from exc
-        if parse_version(pa.__version__) < (14, 0):  # pragma: no cover
+        if parse_version(pa) < (14, 0):  # pragma: no cover
             msg = f"PyArrow>=14.0.0 is required for `DataFrame.__arrow_c_stream__` for object of type {type(native_frame)}"
             raise ModuleNotFoundError(msg) from None
         pa_table = self.to_arrow()
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 618b2c81..8a045d9e 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -253,7 +253,7 @@ def _new_series_impl(
                 narwhals_to_native_dtype as pandas_like_narwhals_to_native_dtype,
             )
 
-            backend_version = parse_version(native_namespace.__version__)
+            backend_version = parse_version(native_namespace)
             pd_dtype = pandas_like_narwhals_to_native_dtype(
                 dtype, None, implementation, backend_version, version
             )
@@ -519,7 +519,7 @@ def _from_numpy_impl(
                 narwhals_to_native_dtype as pandas_like_narwhals_to_native_dtype,
             )
 
-            backend_version = parse_version(native_namespace.__version__)
+            backend_version = parse_version(native_namespace)
             pd_schema = {
                 name: pandas_like_narwhals_to_native_dtype(
                     dtype=schema[name],
@@ -623,9 +623,7 @@ def from_arrow(
         raise TypeError(msg)
     implementation = Implementation.from_native_namespace(native_namespace)
 
-    if implementation is Implementation.POLARS and parse_version(
-        native_namespace.__version__
-    ) >= (1, 3):
+    if implementation.is_polars() and parse_version(native_namespace) >= (1, 3):
         native_frame = native_namespace.DataFrame(native_frame)
     elif implementation in {
         Implementation.PANDAS,
@@ -640,7 +638,7 @@ def from_arrow(
         except ModuleNotFoundError as exc:  # pragma: no cover
             msg = f"PyArrow>=14.0.0 is required for `from_arrow` for object of type {native_namespace}"
             raise ModuleNotFoundError(msg) from exc
-        if parse_version(pa.__version__) < (14, 0):  # pragma: no cover
+        if parse_version(pa) < (14, 0):  # pragma: no cover
             msg = f"PyArrow>=14.0.0 is required for `from_arrow` for object of type {native_namespace}"
             raise ModuleNotFoundError(msg) from None
 
diff --git a/narwhals/schema.py b/narwhals/schema.py
index 8749ef4b..e6cb0ddd 100644
--- a/narwhals/schema.py
+++ b/narwhals/schema.py
@@ -150,7 +150,7 @@ class Schema(BaseSchema):
         to_native_dtype = partial(
             narwhals_to_native_dtype,
             implementation=Implementation.PANDAS,
-            backend_version=parse_version(pd.__version__),
+            backend_version=parse_version(pd),
             version=self._version,
         )
         if dtype_backend is None or isinstance(dtype_backend, str):
@@ -206,6 +206,6 @@ class Schema(BaseSchema):
             (name, narwhals_to_native_dtype(dtype, self._version))
             for name, dtype in self.items()
         )
-        if parse_version(pl.__version__) >= (1, 0, 0):
+        if parse_version(pl) >= (1, 0, 0):
             return pl.Schema(schema)
         return cast("pl.Schema", dict(schema))  # pragma: no cover
diff --git a/narwhals/series.py b/narwhals/series.py
index 777abb98..084d39cb 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -179,7 +179,7 @@ class Series(Generic[IntoSeriesT]):
         except ModuleNotFoundError as exc:  # pragma: no cover
             msg = f"PyArrow>=16.0.0 is required for `Series.__arrow_c_stream__` for object of type {type(native_series)}"
             raise ModuleNotFoundError(msg) from exc
-        if parse_version(pa.__version__) < (16, 0):  # pragma: no cover
+        if parse_version(pa) < (16, 0):  # pragma: no cover
             msg = f"PyArrow>=16.0.0 is required for `Series.__arrow_c_stream__` for object of type {type(native_series)}"
             raise ModuleNotFoundError(msg)
         ca = pa.chunked_array([self.to_arrow()])  # type: ignore[call-overload, unused-ignore]
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 4fae1433..e4c08507 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -399,7 +399,7 @@ def _from_native_impl(  # noqa: PLR0915
             raise TypeError(msg)
         import sqlframe._version
 
-        backend_version = parse_version(sqlframe._version.__version__)
+        backend_version = parse_version(sqlframe._version)
         return LazyFrame(
             SparkLikeLazyFrame(
                 native_object,
@@ -459,9 +459,7 @@ def _from_native_impl(  # noqa: PLR0915
         pl = get_polars()
         return DataFrame(
             PolarsDataFrame(
-                native_object,
-                backend_version=parse_version(pl.__version__),
-                version=version,
+                native_object, backend_version=parse_version(pl), version=version
             ),
             level="full",
         )
@@ -481,9 +479,7 @@ def _from_native_impl(  # noqa: PLR0915
         pl = get_polars()
         return LazyFrame(
             PolarsLazyFrame(
-                native_object,
-                backend_version=parse_version(pl.__version__),
-                version=version,
+                native_object, backend_version=parse_version(pl), version=version
             ),
             level="lazy",
         )
@@ -498,9 +494,7 @@ def _from_native_impl(  # noqa: PLR0915
             return native_object
         return Series(
             PolarsSeries(
-                native_object,
-                backend_version=parse_version(pl.__version__),
-                version=version,
+                native_object, backend_version=parse_version(pl), version=version
             ),
             level="full",
         )
@@ -518,7 +512,7 @@ def _from_native_impl(  # noqa: PLR0915
         return DataFrame(
             PandasLikeDataFrame(
                 native_object,
-                backend_version=parse_version(pd.__version__),
+                backend_version=parse_version(pd),
                 implementation=Implementation.PANDAS,
                 version=version,
                 validate_column_names=True,
@@ -538,7 +532,7 @@ def _from_native_impl(  # noqa: PLR0915
             PandasLikeSeries(
                 native_object,
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd.__version__),
+                backend_version=parse_version(pd),
                 version=version,
             ),
             level="full",
@@ -558,7 +552,7 @@ def _from_native_impl(  # noqa: PLR0915
             PandasLikeDataFrame(
                 native_object,
                 implementation=Implementation.MODIN,
-                backend_version=parse_version(mpd.__version__),
+                backend_version=parse_version(mpd),
                 version=version,
                 validate_column_names=True,
             ),
@@ -577,7 +571,7 @@ def _from_native_impl(  # noqa: PLR0915
             PandasLikeSeries(
                 native_object,
                 implementation=Implementation.MODIN,
-                backend_version=parse_version(mpd.__version__),
+                backend_version=parse_version(mpd),
                 version=version,
             ),
             level="full",
@@ -597,7 +591,7 @@ def _from_native_impl(  # noqa: PLR0915
             PandasLikeDataFrame(
                 native_object,
                 implementation=Implementation.CUDF,
-                backend_version=parse_version(cudf.__version__),
+                backend_version=parse_version(cudf),
                 version=version,
                 validate_column_names=True,
             ),
@@ -616,7 +610,7 @@ def _from_native_impl(  # noqa: PLR0915
             PandasLikeSeries(
                 native_object,
                 implementation=Implementation.CUDF,
-                backend_version=parse_version(cudf.__version__),
+                backend_version=parse_version(cudf),
                 version=version,
             ),
             level="full",
@@ -635,7 +629,7 @@ def _from_native_impl(  # noqa: PLR0915
         return DataFrame(
             ArrowDataFrame(
                 native_object,
-                backend_version=parse_version(pa.__version__),
+                backend_version=parse_version(pa),
                 version=version,
                 validate_column_names=True,
             ),
@@ -652,10 +646,7 @@ def _from_native_impl(  # noqa: PLR0915
             return native_object
         return Series(
             ArrowSeries(
-                native_object,
-                backend_version=parse_version(pa.__version__),
-                name="",
-                version=version,
+                native_object, backend_version=parse_version(pa), name="", version=version
             ),
             level="full",
         )
@@ -675,15 +666,14 @@ def _from_native_impl(  # noqa: PLR0915
                 raise TypeError(msg)
             return native_object
         if (
-            parse_version(get_dask().__version__) <= (2024, 12, 1)
-            and get_dask_expr() is None
+            parse_version(get_dask()) <= (2024, 12, 1) and get_dask_expr() is None
         ):  # pragma: no cover
             msg = "Please install dask-expr"
             raise ImportError(msg)
         return LazyFrame(
             DaskLazyFrame(
                 native_object,
-                backend_version=parse_version(get_dask().__version__),
+                backend_version=parse_version(get_dask()),
                 version=version,
                 validate_column_names=True,
             ),
@@ -705,7 +695,7 @@ def _from_native_impl(  # noqa: PLR0915
             raise TypeError(msg)
         import duckdb  # ignore-banned-import
 
-        backend_version = parse_version(duckdb.__version__)
+        backend_version = parse_version(duckdb)
         if version is Version.V1:
             return DataFrame(
                 DuckDBLazyFrame(
@@ -740,7 +730,7 @@ def _from_native_impl(  # noqa: PLR0915
             return native_object
         import ibis  # ignore-banned-import
 
-        backend_version = parse_version(ibis.__version__)
+        backend_version = parse_version(ibis)
         if version is Version.V1:
             return DataFrame(
                 IbisLazyFrame(
@@ -768,7 +758,7 @@ def _from_native_impl(  # noqa: PLR0915
         return LazyFrame(
             SparkLikeLazyFrame(
                 native_object,
-                backend_version=parse_version(get_pyspark().__version__),
+                backend_version=parse_version(get_pyspark()),
                 version=version,
                 implementation=Implementation.PYSPARK,
             ),
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 33aefc16..2a25ac4c 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -37,6 +37,7 @@ from narwhals.exceptions import InvalidOperationError
 
 if TYPE_CHECKING:
     from types import ModuleType
+    from typing import Protocol
 
     import pandas as pd
     from typing_extensions import Self
@@ -61,6 +62,9 @@ if TYPE_CHECKING:
         "FrameOrSeriesT", bound=Union[LazyFrame[Any], DataFrame[Any], Series[Any]]
     )
 
+    class _SupportsVersion(Protocol):
+        __version__: str
+
 
 class Version(Enum):
     V1 = auto()
@@ -450,11 +454,11 @@ def _is_iterable(arg: Any | Iterable[Any]) -> bool:
     return isinstance(arg, Iterable) and not isinstance(arg, (str, bytes, Series))
 
 
-def parse_version(version: str) -> tuple[int, ...]:
+def parse_version(version: str | ModuleType | _SupportsVersion) -> tuple[int, ...]:
     """Simple version parser; split into a tuple of ints for comparison.
 
     Arguments:
-        version: Version string to parse.
+        version: Version string, or object with one, to parse.
 
     Returns:
         Parsed version number.
@@ -462,8 +466,9 @@ def parse_version(version: str) -> tuple[int, ...]:
     # lifted from Polars
     # [marco]: Take care of DuckDB pre-releases which end with e.g. `-dev4108`
     # and pandas pre-releases which end with e.g. .dev0+618.gb552dc95c9
-    version = re.sub(r"(\D?dev.*$)", "", version)
-    return tuple(int(re.sub(r"\D", "", v)) for v in version.split("."))
+    version_str = version if isinstance(version, str) else version.__version__
+    version_str = re.sub(r"(\D?dev.*$)", "", version_str)
+    return tuple(int(re.sub(r"\D", "", v)) for v in version_str.split("."))
 
 
 def isinstance_or_issubclass(obj_or_cls: object | type, cls_or_tuple: Any) -> bool:


Test patch:
diff --git a/tests/utils_test.py b/tests/utils_test.py
index 9748a954..66046186 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import re
 import string
+from dataclasses import dataclass
 from typing import TYPE_CHECKING
 
 import hypothesis.strategies as st
@@ -23,6 +24,12 @@ from tests.utils import get_module_version_as_tuple
 if TYPE_CHECKING:
     from narwhals.series import Series
     from narwhals.typing import IntoSeriesT
+    from narwhals.utils import _SupportsVersion
+
+
+@dataclass
+class DummyModule:
+    __version__: str
 
 
 def test_maybe_align_index_pandas() -> None:
@@ -283,9 +290,12 @@ def test_generate_temporary_column_name_raise() -> None:
         ("2020.1.2", (2020, 1, 2)),
         ("2020.1.2-dev123", (2020, 1, 2)),
         ("3.0.0.dev0+618.gb552dc95c9", (3, 0, 0)),
+        (DummyModule("2020.1.2-dev123"), (2020, 1, 2)),
     ],
 )
-def test_parse_version(version: str, expected: tuple[int, ...]) -> None:
+def test_parse_version(
+    version: str | _SupportsVersion, expected: tuple[int, ...]
+) -> None:
     assert parse_version(version) == expected