Repository: narwhals-dev/narwhals
Pull request #430 (2024-07-07 12:17:47)

perf: push down `parse_version`
<!--
# 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 # 
- Closes #

## 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/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 39f429eb..14a08050 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -23,14 +23,17 @@ if TYPE_CHECKING:
 
 class ArrowDataFrame:
     # --- not in the spec ---
-    def __init__(self, native_dataframe: Any) -> None:
+    def __init__(
+        self, native_dataframe: Any, *, backend_version: tuple[int, ...]
+    ) -> None:
         self._native_dataframe = native_dataframe
         self._implementation = "arrow"  # for compatibility with PandasDataFrame
+        self._backend_version = backend_version
 
     def __narwhals_namespace__(self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
-        return ArrowNamespace()
+        return ArrowNamespace(backend_version=self._backend_version)
 
     def __native_namespace__(self) -> Any:
         return get_pyarrow()
@@ -42,7 +45,7 @@ class ArrowDataFrame:
         return self
 
     def _from_native_dataframe(self, df: Any) -> Self:
-        return self.__class__(df)
+        return self.__class__(df, backend_version=self._backend_version)
 
     @property
     def shape(self) -> tuple[int, int]:
@@ -69,7 +72,11 @@ class ArrowDataFrame:
         if isinstance(item, str):
             from narwhals._arrow.series import ArrowSeries
 
-            return ArrowSeries(self._native_dataframe[item], name=item)
+            return ArrowSeries(
+                self._native_dataframe[item],
+                name=item,
+                backend_version=self._backend_version,
+            )
 
         elif isinstance(item, slice):
             if item.step is not None and item.step != 1:
@@ -154,7 +161,9 @@ class ArrowDataFrame:
         return self
 
     def collect(self) -> ArrowDataFrame:
-        return ArrowDataFrame(self._native_dataframe)
+        return ArrowDataFrame(
+            self._native_dataframe, backend_version=self._backend_version
+        )
 
     def clone(self) -> Self:
         raise NotImplementedError("clone is not yet supported on PyArrow tables")
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 2fec1724..1196ec62 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -24,6 +24,7 @@ class ArrowExpr:
         function_name: str,
         root_names: list[str] | None,
         output_names: list[str] | None,
+        backend_version: tuple[int, ...],
     ) -> None:
         self._call = call
         self._depth = depth
@@ -32,6 +33,7 @@ class ArrowExpr:
         self._depth = depth
         self._output_names = output_names
         self._implementation = "arrow"
+        self._backend_version = backend_version
 
     def __repr__(self) -> str:  # pragma: no cover
         return (
@@ -43,7 +45,9 @@ class ArrowExpr:
         )
 
     @classmethod
-    def from_column_names(cls: type[Self], *column_names: str) -> Self:
+    def from_column_names(
+        cls: type[Self], *column_names: str, backend_version: tuple[int, ...]
+    ) -> Self:
         from narwhals._arrow.series import ArrowSeries
 
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
@@ -51,6 +55,7 @@ class ArrowExpr:
                 ArrowSeries(
                     df._native_dataframe[column_name],
                     name=column_name,
+                    backend_version=df._backend_version,
                 )
                 for column_name in column_names
             ]
@@ -61,12 +66,13 @@ class ArrowExpr:
             function_name="col",
             root_names=list(column_names),
             output_names=list(column_names),
+            backend_version=backend_version,
         )
 
     def __narwhals_namespace__(self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
-        return ArrowNamespace()
+        return ArrowNamespace(backend_version=self._backend_version)
 
     def cast(self, dtype: DType) -> Self:
         return reuse_series_implementation(self, "cast", dtype)  # type: ignore[type-var]
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index 7aae0c4d..c7f476c5 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -54,6 +54,7 @@ class ArrowNamespace:
             function_name=function_name,
             root_names=root_names,
             output_names=output_names,
+            backend_version=self._backend_version,
         )
 
     def _create_expr_from_series(self, series: ArrowSeries) -> ArrowExpr:
@@ -65,6 +66,7 @@ class ArrowNamespace:
             function_name="series",
             root_names=None,
             output_names=None,
+            backend_version=self._backend_version,
         )
 
     def _create_series_from_scalar(self, value: Any, series: ArrowSeries) -> ArrowSeries:
@@ -73,16 +75,20 @@ class ArrowNamespace:
         return ArrowSeries._from_iterable(
             [value],
             name=series.name,
+            backend_version=self._backend_version,
         )
 
     # --- not in spec ---
-    def __init__(self) -> None: ...
+    def __init__(self, *, backend_version: tuple[int, ...]) -> None:
+        self._backend_version = backend_version
 
     # --- selection ---
     def col(self, *column_names: str | Iterable[str]) -> ArrowExpr:
         from narwhals._arrow.expr import ArrowExpr
 
-        return ArrowExpr.from_column_names(*flatten(column_names))
+        return ArrowExpr.from_column_names(
+            *flatten(column_names), backend_version=self._backend_version
+        )
 
     def all(self) -> ArrowExpr:
         from narwhals._arrow.expr import ArrowExpr
@@ -90,11 +96,16 @@ class ArrowNamespace:
 
         return ArrowExpr(
             lambda df: [
-                ArrowSeries(df._native_dataframe[column_name], name=column_name)
+                ArrowSeries(
+                    df._native_dataframe[column_name],
+                    name=column_name,
+                    backend_version=df._backend_version,
+                )
                 for column_name in df.columns
             ],
             depth=0,
             function_name="all",
             root_names=None,
             output_names=None,
+            backend_version=self._backend_version,
         )
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index f58a9890..fca21483 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -19,35 +19,44 @@ if TYPE_CHECKING:
 
 class ArrowSeries:
     def __init__(
-        self,
-        native_series: Any,
-        *,
-        name: str,
+        self, native_series: Any, *, name: str, backend_version: tuple[int, ...]
     ) -> None:
         self._name = name
         self._native_series = native_series
         self._implementation = "arrow"  # for compatibility with PandasSeries
+        self._backend_version = backend_version
 
     def _from_native_series(self, series: Any) -> Self:
         return self.__class__(
             series,
             name=self._name,
+            backend_version=self._backend_version,
         )
 
     @classmethod
-    def _from_iterable(cls: type[Self], data: Iterable[Any], name: str) -> Self:
+    def _from_iterable(
+        cls: type[Self],
+        data: Iterable[Any],
+        name: str,
+        *,
+        backend_version: tuple[int, ...],
+    ) -> Self:
         return cls(
             native_series_from_iterable(
-                data, name=name, index=None, implementation="arrow"
+                data,
+                name=name,
+                index=None,
+                implementation="arrow",
             ),
             name=name,
+            backend_version=backend_version,
         )
 
     def __len__(self) -> int:
         return len(self._native_series)
 
     def __narwhals_namespace__(self) -> ArrowNamespace:
-        return ArrowNamespace()
+        return ArrowNamespace(backend_version=self._backend_version)
 
     @property
     def name(self) -> str:
@@ -72,6 +81,7 @@ class ArrowSeries:
         return self.__class__(
             self._native_series,
             name=name,
+            backend_version=self._backend_version,
         )
 
     @property
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index bae7120e..5fac8b60 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -22,7 +22,6 @@ from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
 from narwhals.utils import flatten
-from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -41,10 +40,12 @@ class PandasDataFrame:
         native_dataframe: Any,
         *,
         implementation: str,
+        backend_version: tuple[int, ...],
     ) -> None:
         self._validate_columns(native_dataframe.columns)
         self._native_dataframe = native_dataframe
         self._implementation = implementation
+        self._backend_version = backend_version
 
     def __narwhals_dataframe__(self) -> Self:
         return self
@@ -55,7 +56,7 @@ class PandasDataFrame:
     def __narwhals_namespace__(self) -> PandasNamespace:
         from narwhals._pandas_like.namespace import PandasNamespace
 
-        return PandasNamespace(self._implementation)
+        return PandasNamespace(self._implementation, self._backend_version)
 
     def __native_namespace__(self) -> Any:
         if self._implementation == "pandas":
@@ -85,6 +86,7 @@ class PandasDataFrame:
         return self.__class__(
             df,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     @overload
@@ -100,13 +102,16 @@ class PandasDataFrame:
             return PandasSeries(
                 self._native_dataframe.loc[:, item],
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
 
         elif isinstance(item, (slice, Sequence)):
             from narwhals._pandas_like.dataframe import PandasDataFrame
 
             return PandasDataFrame(
-                self._native_dataframe.iloc[item], implementation=self._implementation
+                self._native_dataframe.iloc[item],
+                implementation=self._implementation,
+                backend_version=self._backend_version,
             )
         elif (
             (np := get_numpy()) is not None
@@ -171,6 +176,7 @@ class PandasDataFrame:
         df = horizontal_concat(
             new_series,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
         return self._from_native_dataframe(df)
 
@@ -182,11 +188,13 @@ class PandasDataFrame:
             range(len(self._native_dataframe)),
             index=self._native_dataframe.index,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         ).alias(name)
         return self._from_native_dataframe(
             horizontal_concat(
                 [row_index._native_series, self._native_dataframe],
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
         )
 
@@ -196,7 +204,7 @@ class PandasDataFrame:
     ) -> Self:
         from narwhals._pandas_like.namespace import PandasNamespace
 
-        plx = PandasNamespace(self._implementation)
+        plx = PandasNamespace(self._implementation, self._backend_version)
         expr = plx.all_horizontal(*predicates)
         # Safety: all_horizontal's expression only returns a single column.
         mask = expr._call(self)[0]
@@ -242,6 +250,7 @@ class PandasDataFrame:
             df = horizontal_concat(
                 to_concat,
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
         else:
             df = self._native_dataframe.assign(
@@ -277,6 +286,7 @@ class PandasDataFrame:
         return PandasDataFrame(
             self._native_dataframe,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     # --- actions ---
@@ -303,9 +313,7 @@ class PandasDataFrame:
 
         if how == "cross":
             if self._implementation in {"modin", "cudf"} or (
-                self._implementation == "pandas"
-                and (pd := get_pandas()) is not None
-                and parse_version(pd.__version__) < parse_version("1.4.0")
+                self._implementation == "pandas" and self._backend_version < (1, 4)
             ):
                 key_token = generate_unique_token(
                     n_bytes=8, columns=[*self.columns, *other.columns]
@@ -423,6 +431,7 @@ class PandasDataFrame:
         return PandasSeries(
             self._native_dataframe.duplicated(keep=False),
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def is_empty(self: Self) -> bool:
@@ -434,12 +443,14 @@ class PandasDataFrame:
         return PandasSeries(
             ~self._native_dataframe.duplicated(keep=False),
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def null_count(self: Self) -> PandasDataFrame:
         return PandasDataFrame(
             self._native_dataframe.isnull().sum(axis=0).to_frame().transpose(),
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def item(self: Self, row: int | None = None, column: int | str | None = None) -> Any:
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index f97cce10..a7d62d0c 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -26,6 +26,7 @@ class PandasExpr:
         root_names: list[str] | None,
         output_names: list[str] | None,
         implementation: str,
+        backend_version: tuple[int, ...],
     ) -> None:
         self._call = call
         self._depth = depth
@@ -34,6 +35,7 @@ class PandasExpr:
         self._depth = depth
         self._output_names = output_names
         self._implementation = implementation
+        self._backend_version = backend_version
 
     def __repr__(self) -> str:  # pragma: no cover
         return (
@@ -47,17 +49,21 @@ class PandasExpr:
     def __narwhals_namespace__(self) -> PandasNamespace:
         from narwhals._pandas_like.namespace import PandasNamespace
 
-        return PandasNamespace(self._implementation)
+        return PandasNamespace(self._implementation, self._backend_version)
 
     @classmethod
     def from_column_names(
-        cls: type[Self], *column_names: str, implementation: str
+        cls: type[Self],
+        *column_names: str,
+        implementation: str,
+        backend_version: tuple[int, ...],
     ) -> Self:
         def func(df: PandasDataFrame) -> list[PandasSeries]:
             return [
                 PandasSeries(
                     df._native_dataframe.loc[:, column_name],
                     implementation=df._implementation,
+                    backend_version=df._backend_version,
                 )
                 for column_name in column_names
             ]
@@ -69,6 +75,7 @@ class PandasExpr:
             root_names=list(column_names),
             output_names=list(column_names),
             implementation=implementation,
+            backend_version=backend_version,
         )
 
     def cast(
@@ -206,7 +213,7 @@ class PandasExpr:
     def filter(self, *predicates: Any) -> Self:
         from narwhals._pandas_like.namespace import PandasNamespace
 
-        plx = PandasNamespace(self._implementation)
+        plx = PandasNamespace(self._implementation, self._backend_version)
         expr = plx.all_horizontal(*predicates)
         return reuse_series_implementation(self, "filter", other=expr)
 
@@ -252,6 +259,7 @@ class PandasExpr:
             root_names=self._root_names,
             output_names=[name],
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def over(self, keys: list[str]) -> Self:
@@ -274,6 +282,7 @@ class PandasExpr:
             root_names=self._root_names,
             output_names=self._output_names,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def is_duplicated(self) -> Self:
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 45019f80..538c6a44 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -12,8 +12,6 @@ from typing import Iterator
 from narwhals._pandas_like.utils import is_simple_aggregation
 from narwhals._pandas_like.utils import native_series_from_iterable
 from narwhals._pandas_like.utils import parse_into_exprs
-from narwhals.dependencies import get_pandas
-from narwhals.utils import parse_version
 from narwhals.utils import remove_prefix
 
 if TYPE_CHECKING:
@@ -44,6 +42,7 @@ class PandasGroupBy:
         exprs = parse_into_exprs(
             self._df._implementation,
             *aggs,
+            backend_version=self._df._backend_version,
             **named_aggs,
         )
         implementation: str = self._df._implementation
@@ -64,8 +63,9 @@ class PandasGroupBy:
             self._keys,
             output_names,
             self._from_native_dataframe,
-            implementation,
             dataframe_is_empty=self._df._native_dataframe.empty,
+            implementation=implementation,
+            backend_version=self._df._backend_version,
         )
 
     def _from_native_dataframe(self, df: PandasDataFrame) -> PandasDataFrame:
@@ -74,6 +74,7 @@ class PandasGroupBy:
         return PandasDataFrame(
             df,
             implementation=self._df._implementation,
+            backend_version=self._df._backend_version,
         )
 
     def __iter__(self) -> Iterator[tuple[Any, PandasDataFrame]]:
@@ -86,8 +87,9 @@ def agg_pandas(  # noqa: PLR0913
     keys: list[str],
     output_names: list[str],
     from_dataframe: Callable[[Any], PandasDataFrame],
-    implementation: Any,
     *,
+    implementation: Any,
+    backend_version: tuple[int, ...],
     dataframe_is_empty: bool,
 ) -> PandasDataFrame:
     """
@@ -96,8 +98,6 @@ def agg_pandas(  # noqa: PLR0913
     - https://github.com/rapidsai/cudf/issues/15118
     - https://github.com/rapidsai/cudf/issues/15084
     """
-    pd = get_pandas()
-
     all_simple_aggs = True
     for expr in exprs:
         if not is_simple_aggregation(expr):
@@ -174,16 +174,14 @@ def agg_pandas(  # noqa: PLR0913
                 out_group.append(result_keys._native_series.iloc[0])
                 out_names.append(result_keys.name)
         return native_series_from_iterable(
-            out_group, index=out_names, name="", implementation=implementation
+            out_group,
+            index=out_names,
+            name="",
+            implementation=implementation,
         )
 
-    if implementation == "pandas":
-        pd = get_pandas()
-
-        if parse_version(pd.__version__) < parse_version("2.2.0"):  # pragma: no cover
-            result_complex = grouped.apply(func)
-        else:
-            result_complex = grouped.apply(func, include_groups=False)
+    if implementation == "pandas" and backend_version >= (2, 2):
+        result_complex = grouped.apply(func, include_groups=False)
     else:  # pragma: no cover
         result_complex = grouped.apply(func)
 
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index 3986a8ae..004cdf01 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -43,11 +43,14 @@ class PandasNamespace:
 
     @property
     def selectors(self) -> PandasSelectorNamespace:
-        return PandasSelectorNamespace(self._implementation)
+        return PandasSelectorNamespace(
+            implementation=self._implementation, backend_version=self._backend_version
+        )
 
     # --- not in spec ---
-    def __init__(self, implementation: str) -> None:
+    def __init__(self, implementation: str, backend_version: tuple[int, ...]) -> None:
         self._implementation = implementation
+        self._backend_version = backend_version
 
     def _create_expr_from_callable(  # noqa: PLR0913
         self,
@@ -65,6 +68,7 @@ class PandasNamespace:
             root_names=root_names,
             output_names=output_names,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def _create_series_from_scalar(
@@ -75,6 +79,7 @@ class PandasNamespace:
             name=series._native_series.name,
             index=series._native_series.index[0:1],
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def _create_expr_from_series(self, series: PandasSeries) -> PandasExpr:
@@ -85,12 +90,15 @@ class PandasNamespace:
             root_names=None,
             output_names=None,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     # --- selection ---
     def col(self, *column_names: str | Iterable[str]) -> PandasExpr:
         return PandasExpr.from_column_names(
-            *flatten(column_names), implementation=self._implementation
+            *flatten(column_names),
+            implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def all(self) -> PandasExpr:
@@ -99,6 +107,7 @@ class PandasNamespace:
                 PandasSeries(
                     df._native_dataframe.loc[:, column_name],
                     implementation=self._implementation,
+                    backend_version=self._backend_version,
                 )
                 for column_name in df.columns
             ],
@@ -107,6 +116,7 @@ class PandasNamespace:
             root_names=None,
             output_names=None,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def lit(self, value: Any, dtype: dtypes.DType | None) -> PandasExpr:
@@ -116,6 +126,7 @@ class PandasNamespace:
                 name="lit",
                 index=df._native_dataframe.index[0:1],
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
             if dtype:
                 return pandas_series.cast(dtype)
@@ -128,27 +139,36 @@ class PandasNamespace:
             root_names=None,
             output_names=["lit"],
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     # --- reduction ---
     def sum(self, *column_names: str) -> PandasExpr:
         return PandasExpr.from_column_names(
-            *column_names, implementation=self._implementation
+            *column_names,
+            implementation=self._implementation,
+            backend_version=self._backend_version,
         ).sum()
 
     def mean(self, *column_names: str) -> PandasExpr:
         return PandasExpr.from_column_names(
-            *column_names, implementation=self._implementation
+            *column_names,
+            implementation=self._implementation,
+            backend_version=self._backend_version,
         ).mean()
 
     def max(self, *column_names: str) -> PandasExpr:
         return PandasExpr.from_column_names(
-            *column_names, implementation=self._implementation
+            *column_names,
+            implementation=self._implementation,
+            backend_version=self._backend_version,
         ).max()
 
     def min(self, *column_names: str) -> PandasExpr:
         return PandasExpr.from_column_names(
-            *column_names, implementation=self._implementation
+            *column_names,
+            implementation=self._implementation,
+            backend_version=self._backend_version,
         ).min()
 
     def len(self) -> PandasExpr:
@@ -159,6 +179,7 @@ class PandasNamespace:
                     name="len",
                     index=[0],
                     implementation=self._implementation,
+                    backend_version=self._backend_version,
                 )
             ],
             depth=0,
@@ -166,20 +187,29 @@ class PandasNamespace:
             root_names=None,
             output_names=["len"],
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     # --- horizontal ---
     def sum_horizontal(
         self, *exprs: IntoPandasExpr | Iterable[IntoPandasExpr]
     ) -> PandasExpr:
-        return reduce(lambda x, y: x + y, parse_into_exprs(self._implementation, *exprs))
+        return reduce(
+            lambda x, y: x + y,
+            parse_into_exprs(
+                self._implementation, *exprs, backend_version=self._backend_version
+            ),
+        )
 
     def all_horizontal(
         self, *exprs: IntoPandasExpr | Iterable[IntoPandasExpr]
     ) -> PandasExpr:
         # Why is this showing up as uncovered? It defo is?
         return reduce(
-            lambda x, y: x & y, parse_into_exprs(self._implementation, *exprs)
+            lambda x, y: x & y,
+            parse_into_exprs(
+                self._implementation, *exprs, backend_version=self._backend_version
+            ),
         )  # pragma: no cover
 
     def concat(
@@ -191,12 +221,22 @@ class PandasNamespace:
         dfs: list[Any] = [item._native_dataframe for item in items]
         if how == "horizontal":
             return PandasDataFrame(
-                horizontal_concat(dfs, implementation=self._implementation),
+                horizontal_concat(
+                    dfs,
+                    implementation=self._implementation,
+                    backend_version=self._backend_version,
+                ),
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
         if how == "vertical":
             return PandasDataFrame(
-                vertical_concat(dfs, implementation=self._implementation),
+                vertical_concat(
+                    dfs,
+                    implementation=self._implementation,
+                    backend_version=self._backend_version,
+                ),
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
         raise NotImplementedError
diff --git a/narwhals/_pandas_like/selectors.py b/narwhals/_pandas_like/selectors.py
index 91036b9b..a78f54ca 100644
--- a/narwhals/_pandas_like/selectors.py
+++ b/narwhals/_pandas_like/selectors.py
@@ -14,8 +14,9 @@ if TYPE_CHECKING:
 
 
 class PandasSelectorNamespace:
-    def __init__(self, implementation: str) -> None:
+    def __init__(self, *, implementation: str, backend_version: tuple[int, ...]) -> None:
         self._implementation = implementation
+        self._backend_version = backend_version
 
     def by_dtype(self, dtypes: list[DType | type[DType]]) -> PandasSelector:
         def func(df: PandasDataFrame) -> list[PandasSeries]:
@@ -28,6 +29,7 @@ class PandasSelectorNamespace:
             root_names=None,
             output_names=None,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def numeric(self) -> PandasSelector:
@@ -66,6 +68,7 @@ class PandasSelectorNamespace:
             root_names=None,
             output_names=None,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
 
@@ -87,6 +90,7 @@ class PandasSelector(PandasExpr):
             root_names=self._root_names,
             output_names=self._output_names,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def __sub__(self, other: PandasSelector | Any) -> PandasSelector | Any:
@@ -104,6 +108,7 @@ class PandasSelector(PandasExpr):
                 root_names=None,
                 output_names=None,
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
         else:
             return self._to_expr() - other
@@ -123,6 +128,7 @@ class PandasSelector(PandasExpr):
                 root_names=None,
                 output_names=None,
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
         else:
             return self._to_expr() | other
@@ -142,12 +148,18 @@ class PandasSelector(PandasExpr):
                 root_names=None,
                 output_names=None,
                 implementation=self._implementation,
+                backend_version=self._backend_version,
             )
         else:
             return self._to_expr() & other
 
     def __invert__(self) -> PandasSelector:
-        return PandasSelectorNamespace(self._implementation).all() - self
+        return (
+            PandasSelectorNamespace(
+                implementation=self._implementation, backend_version=self._backend_version
+            ).all()
+            - self
+        )
 
     def __rsub__(self, other: Any) -> NoReturn:
         raise NotImplementedError
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index af0567fc..b428eefd 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -15,7 +15,6 @@ from narwhals._pandas_like.utils import validate_column_comparand
 from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_pandas
-from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -75,29 +74,25 @@ class PandasSeries:
         native_series: Any,
         *,
         implementation: str,
+        backend_version: tuple[int, ...],
     ) -> None:
         self._name = native_series.name
         self._native_series = native_series
         self._implementation = implementation
+        self._backend_version = backend_version
 
         # In pandas, copy-on-write becomes the default in version 3.
         # So, before that, we need to explicitly avoid unnecessary
         # copies by using `copy=False` sometimes.
-        self._use_copy_false = False
-        if self._implementation == "pandas":
-            pd = get_pandas()
-
-            if parse_version(pd.__version__) < parse_version("3.0.0"):
-                self._use_copy_false = True
-            else:  # pragma: no cover
-                pass
-        else:  # pragma: no cover
-            pass
+        if self._implementation == "pandas" and self._backend_version < (3, 0, 0):
+            self._use_copy_false = True
+        else:
+            self._use_copy_false = False
 
     def __narwhals_namespace__(self) -> PandasNamespace:
         from narwhals._pandas_like.namespace import PandasNamespace
 
-        return PandasNamespace(self._implementation)
+        return PandasNamespace(self._implementation, self._backend_version)
 
     def __native_namespace__(self) -> Any:
         if self._implementation == "pandas":
@@ -124,17 +119,28 @@ class PandasSeries:
         return self.__class__(
             series,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     @classmethod
-    def _from_iterable(
-        cls: type[Self], data: Iterable[Any], name: str, index: Any, implementation: str
+    def _from_iterable(  # noqa: PLR0913
+        cls: type[Self],
+        data: Iterable[Any],
+        name: str,
+        index: Any,
+        *,
+        implementation: str,
+        backend_version: tuple[int, ...],
     ) -> Self:
         return cls(
             native_series_from_iterable(
-                data, name=name, index=index, implementation=implementation
+                data,
+                name=name,
+                index=index,
+                implementation=implementation,
             ),
             implementation=implementation,
+            backend_version=backend_version,
         )
 
     def __len__(self) -> int:
@@ -176,7 +182,9 @@ class PandasSeries:
         from narwhals._pandas_like.dataframe import PandasDataFrame
 
         return PandasDataFrame(
-            self._native_series.to_frame(), implementation=self._implementation
+            self._native_series.to_frame(),
+            implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def to_list(self) -> Any:
@@ -454,9 +462,9 @@ class PandasSeries:
             has_missing
             and str(self._native_series.dtype) in PANDAS_TO_NUMPY_DTYPE_MISSING
         ):
-            if self._implementation == "pandas" and parse_version(
-                get_pandas().__version__
-            ) < parse_version("1.0.0"):  # pragma: no cover
+            if self._implementation == "pandas" and self._backend_version < (
+                1,
+            ):  # pragma: no cover
                 kwargs = {}
             else:
                 kwargs = {"na_value": float("nan")}
@@ -531,6 +539,7 @@ class PandasSeries:
         return PandasDataFrame(
             val_count,
             implementation=self._implementation,
+            backend_version=self._backend_version,
         )
 
     def quantile(
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index ee5cc371..e2e52f27 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -14,7 +14,6 @@ from narwhals.dependencies import get_pandas
 from narwhals.dependencies import get_pyarrow
 from narwhals.utils import flatten
 from narwhals.utils import isinstance_or_issubclass
-from narwhals.utils import parse_version
 
 T = TypeVar("T")
 
@@ -56,7 +55,10 @@ def validate_column_comparand(index: Any, other: Any) -> Any:
             return other.item()
         if other._native_series.index is not index:
             return set_axis(
-                other._native_series, index, implementation=other._implementation
+                other._native_series,
+                index,
+                implementation=other._implementation,
+                backend_version=other._backend_version,
             )
         return other._native_series
     return other
@@ -79,7 +81,10 @@ def validate_dataframe_comparand(index: Any, other: Any) -> Any:
             return other._native_series.iloc[0]
         if other._native_series.index is not index:
             return set_axis(
-                other._native_series, index, implementation=other._implementation
+                other._native_series,
+                index,
+                implementation=other._implementation,
+                backend_version=other._backend_version,
             )
         return other._native_series
     raise AssertionError("Please report a bug")
@@ -97,18 +102,31 @@ def maybe_evaluate_expr(df: PandasDataFrame, expr: Any) -> Any:
 def parse_into_exprs(
     implementation: str,
     *exprs: IntoPandasExpr | Iterable[IntoPandasExpr],
+    backend_version: tuple[int, ...],
     **named_exprs: IntoPandasExpr,
 ) -> list[PandasExpr]:
     """Parse each input as an expression (if it's not already one). See `parse_into_expr` for
     more details."""
-    out = [parse_into_expr(implementation, into_expr) for into_expr in flatten(exprs)]
+    out = [
+        parse_into_expr(
+            into_expr, implementation=implementation, backend_version=backend_version
+        )
+        for into_expr in flatten(exprs)
+    ]
     for name, expr in named_exprs.items():
-        out.append(parse_into_expr(implementation, expr).alias(name))
+        out.append(
+            parse_into_expr(
+                expr, implementation=implementation, backend_version=backend_version
+            ).alias(name)
+        )
     return out
 
 
 def parse_into_expr(
-    implementation: str, into_expr: IntoPandasExpr | IntoArrowExpr
+    into_expr: IntoPandasExpr | IntoArrowExpr,
+    *,
+    implementation: str,
+    backend_version: tuple[int, ...],
 ) -> PandasExpr:
     """Parse `into_expr` as an expression.
 
@@ -129,9 +147,13 @@ def parse_into_expr(
     from narwhals._pandas_like.series import PandasSeries
 
     if implementation == "arrow":
-        plx: ArrowNamespace | PandasNamespace = ArrowNamespace()
+        plx: ArrowNamespace | PandasNamespace = ArrowNamespace(
+            backend_version=backend_version
+        )
     else:
-        plx = PandasNamespace(implementation=implementation)
+        plx = PandasNamespace(
+            implementation=implementation, backend_version=backend_version
+        )
     if isinstance(into_expr, (PandasExpr, ArrowExpr)):
         return into_expr  # type: ignore[return-value]
     if isinstance(into_expr, (PandasSeries, ArrowSeries)):
@@ -139,7 +161,9 @@ def parse_into_expr(
     if isinstance(into_expr, str):
         return plx.col(into_expr)  # type: ignore[return-value]
     if (np := get_numpy()) is not None and isinstance(into_expr, np.ndarray):
-        series = create_native_series(into_expr, implementation=implementation)
+        series = create_native_series(
+            into_expr, implementation=implementation, backend_version=backend_version
+        )
         return plx._create_expr_from_series(series)  # type: ignore[arg-type, return-value]
     msg = f"Expected IntoExpr, got {type(into_expr)}"  # pragma: no cover
     raise AssertionError(msg)
@@ -147,8 +171,10 @@ def parse_into_expr(
 
 def create_native_series(
     iterable: Any,
-    implementation: str,
     index: Any = None,
+    *,
+    implementation: str,
+    backend_version: tuple[int, ...],
 ) -> PandasSeries:
     from narwhals._pandas_like.series import PandasSeries
 
@@ -161,7 +187,9 @@ def create_native_series(
     elif implementation == "cudf":
         cudf = get_cudf()
         series = cudf.Series(iterable, index=index, name="")
-    return PandasSeries(series, implementation=implementation)
+    return PandasSeries(
+        series, implementation=implementation, backend_version=backend_version
+    )
 
 
 def evaluate_into_expr(
@@ -170,7 +198,9 @@ def evaluate_into_expr(
     """
     Return list of raw columns.
     """
-    expr = parse_into_expr(df._implementation, into_expr)
+    expr = parse_into_expr(
+        into_expr, implementation=df._implementation, backend_version=df._backend_version
+    )
     return expr._call(df)
 
 
@@ -278,6 +308,7 @@ def reuse_series_namespace_implementation(
         root_names=expr._root_names,
         output_names=expr._output_names,
         implementation=expr._implementation,
+        backend_version=expr._backend_version,
     )
 
 
@@ -298,7 +329,9 @@ def is_simple_aggregation(expr: PandasExpr) -> bool:
     return expr._depth < 2
 
 
-def horizontal_concat(dfs: list[Any], implementation: str) -> Any:
+def horizontal_concat(
+    dfs: list[Any], *, implementation: str, backend_version: tuple[int, ...]
+) -> Any:
     """
     Concatenate (native) DataFrames horizontally.
 
@@ -307,7 +340,7 @@ def horizontal_concat(dfs: list[Any], implementation: str) -> Any:
     if implementation == "pandas":
         pd = get_pandas()
 
-        if parse_version(pd.__version__) < parse_version("3.0.0"):
+        if backend_version < (3,):
             return pd.concat(dfs, axis=1, copy=False)
         return pd.concat(dfs, axis=1)  # pragma: no cover
     if implementation == "cudf":  # pragma: no cover
@@ -322,7 +355,9 @@ def horizontal_concat(dfs: list[Any], implementation: str) -> Any:
     raise TypeError(msg)  # pragma: no cover
 
 
-def vertical_concat(dfs: list[Any], implementation: str) -> Any:
+def vertical_concat(
+    dfs: list[Any], *, implementation: str, backend_version: tuple[int, ...]
+) -> Any:
     """
     Concatenate (native) DataFrames vertically.
 
@@ -340,7 +375,7 @@ def vertical_concat(dfs: list[Any], implementation: str) -> Any:
     if implementation == "pandas":
         pd = get_pandas()
 
-        if parse_version(pd.__version__) < parse_version("3.0.0"):
+        if backend_version < (3,):
             return pd.concat(dfs, axis=0, copy=False)
         return pd.concat(dfs, axis=0)  # pragma: no cover
     if implementation == "cudf":  # pragma: no cover
@@ -356,7 +391,10 @@ def vertical_concat(dfs: list[Any], implementation: str) -> Any:
 
 
 def native_series_from_iterable(
-    data: Iterable[Any], name: str, index: Any, implementation: str
+    data: Iterable[Any],
+    name: str,
+    index: Any,
+    implementation: str,
 ) -> Any:
     """Return native series."""
     if implementation == "pandas":
@@ -378,16 +416,14 @@ def native_series_from_iterable(
     raise TypeError(msg)  # pragma: no cover
 
 
-def set_axis(obj: T, index: Any, implementation: str) -> T:
-    if implementation == "pandas" and parse_version(
-        get_pandas().__version__
-    ) < parse_version("1.0.0"):  # pragma: no cover
+def set_axis(
+    obj: T, index: Any, *, implementation: str, backend_version: tuple[int, ...]
+) -> T:
+    if implementation == "pandas" and backend_version < (1,):  # pragma: no cover
         kwargs = {"inplace": False}
     else:
         kwargs = {}
-    if implementation == "pandas" and parse_version(
-        get_pandas().__version__
-    ) >= parse_version("1.5.0"):
+    if implementation == "pandas" and backend_version >= (1, 5):  # pragma: no cover
         kwargs["copy"] = False
     else:  # pragma: no cover
         pass
@@ -602,7 +638,14 @@ def validate_indices(series: list[PandasSeries]) -> list[Any]:
     reindexed = [series[0]._native_series]
     for s in series[1:]:
         if s._native_series.index is not idx:
-            reindexed.append(set_axis(s._native_series, idx, s._implementation))
+            reindexed.append(
+                set_axis(
+                    s._native_series,
+                    idx,
+                    implementation=s._implementation,
+                    backend_version=s._backend_version,
+                )
+            )
         else:
             reindexed.append(s._native_series)
     return reindexed
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index d0cc15ee..33c542ed 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -44,6 +44,7 @@ FrameT = TypeVar("FrameT", bound="IntoDataFrame")
 class BaseFrame(Generic[FrameT]):
     _dataframe: Any
     _is_polars: bool
+    _backend_version: tuple[int, ...]
 
     def __len__(self) -> Any:
         return self._dataframe.__len__()
@@ -63,6 +64,7 @@ class BaseFrame(Generic[FrameT]):
         return self.__class__(  # type: ignore[call-arg]
             df,
             is_polars=self._is_polars,
+            backend_version=self._backend_version,
         )
 
     def _flatten_and_extract(self, *args: Any, **kwargs: Any) -> Any:
@@ -102,9 +104,7 @@ class BaseFrame(Generic[FrameT]):
         return function(self, *args, **kwargs)
 
     def with_row_index(self, name: str = "index") -> Self:
-        if self._is_polars and parse_version(get_polars().__version__) < parse_version(
-            "0.20.4"
-        ):  # pragma: no cover
+        if self._is_polars and self._backend_version < (0, 20, 4):  # pragma: no cover
             return self._from_dataframe(
                 self._dataframe.with_row_count(name),
             )
@@ -124,6 +124,8 @@ class BaseFrame(Generic[FrameT]):
     def lazy(self) -> LazyFrame[Any]:
         return LazyFrame(
             self._dataframe.lazy(),
+            is_polars=self._is_polars,
+            backend_version=self._backend_version,
         )
 
     def with_columns(
@@ -220,34 +222,41 @@ class DataFrame(BaseFrame[FrameT]):
         self,
         df: Any,
         *,
-        is_polars: bool = False,
+        backend_version: tuple[int, ...],
+        is_polars: bool,
     ) -> None:
         self._is_polars = is_polars
+        self._backend_version = backend_version
         if hasattr(df, "__narwhals_dataframe__"):
             self._dataframe: Any = df.__narwhals_dataframe__()
-        elif is_polars or (
-            (pl := get_polars()) is not None and isinstance(df, pl.DataFrame)
-        ):
+        elif is_polars and isinstance(df, get_polars().DataFrame):
             self._dataframe = df
-            self._is_polars = True
-        elif (pl := get_polars()) is not None and isinstance(df, pl.LazyFrame):
+        elif is_polars and isinstance(df, get_polars().LazyFrame):
             raise TypeError(
                 "Can't instantiate DataFrame from Polars LazyFrame. Call `collect()` first, or use `narwhals.LazyFrame` if you don't specifically require eager execution."
             )
         elif (pd := get_pandas()) is not None and isinstance(df, pd.DataFrame):
-            self._dataframe = PandasDataFrame(df, implementation="pandas")
+            self._dataframe = PandasDataFrame(
+                df, implementation="pandas", backend_version=parse_version(pd.__version__)
+            )
         elif (mpd := get_modin()) is not None and isinstance(
             df, mpd.DataFrame
         ):  # pragma: no cover
-            self._dataframe = PandasDataFrame(df, implementation="modin")
+            self._dataframe = PandasDataFrame(
+                df, implementation="modin", backend_version=parse_version(mpd.__version__)
+            )
         elif (cudf := get_cudf()) is not None and isinstance(
             df, cudf.DataFrame
         ):  # pragma: no cover
-            self._dataframe = PandasDataFrame(df, implementation="cudf")
+            self._dataframe = PandasDataFrame(
+                df, implementation="cudf", backend_version=parse_version(cudf.__version__)
+            )
         elif (pa := get_pyarrow()) is not None and isinstance(
             df, pa.Table
         ):  # pragma: no cover
-            self._dataframe = ArrowDataFrame(df)
+            self._dataframe = ArrowDataFrame(
+                df, backend_version=parse_version(pa.__version__)
+            )
         else:
             msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(df)}"
             raise TypeError(msg)
@@ -443,7 +452,11 @@ class DataFrame(BaseFrame[FrameT]):
         if isinstance(item, str):
             from narwhals.series import Series
 
-            return Series(self._dataframe[item])
+            return Series(
+                self._dataframe[item],
+                backend_version=self._backend_version,
+                is_polars=self._is_polars,
+            )
 
         elif isinstance(item, (Sequence, slice)) or (
             (np := get_numpy()) is not None
@@ -504,7 +517,11 @@ class DataFrame(BaseFrame[FrameT]):
 
         if as_series:
             return {
-                key: Series(value)
+                key: Series(
+                    value,
+                    backend_version=self._backend_version,
+                    is_polars=self._is_polars,
+                )
                 for key, value in self._dataframe.to_dict(as_series=as_series).items()
             }
         return self._dataframe.to_dict(as_series=as_series)  # type: ignore[no-any-return]
@@ -1610,7 +1627,11 @@ class DataFrame(BaseFrame[FrameT]):
         """
         from narwhals.series import Series
 
-        return Series(self._dataframe.is_duplicated())
+        return Series(
+            self._dataframe.is_duplicated(),
+            backend_version=self._backend_version,
+            is_polars=self._is_polars,
+        )
 
     def is_empty(self: Self) -> bool:
         r"""
@@ -1692,7 +1713,11 @@ class DataFrame(BaseFrame[FrameT]):
         """
         from narwhals.series import Series
 
-        return Series(self._dataframe.is_unique())
+        return Series(
+            self._dataframe.is_unique(),
+            backend_version=self._backend_version,
+            is_polars=self._is_polars,
+        )
 
     def null_count(self: Self) -> Self:
         r"""
@@ -1831,9 +1856,11 @@ class LazyFrame(BaseFrame[FrameT]):
         self,
         df: Any,
         *,
-        is_polars: bool = False,
+        is_polars: bool,
+        backend_version: tuple[int, ...],
     ) -> None:
         self._is_polars = is_polars
+        self._backend_version = backend_version
         if hasattr(df, "__narwhals_lazyframe__"):
             self._dataframe: Any = df.__narwhals_lazyframe__()
         elif is_polars or (
@@ -1900,6 +1927,8 @@ class LazyFrame(BaseFrame[FrameT]):
         """
         return DataFrame(
             self._dataframe.collect(),
+            is_polars=self._is_polars,
+            backend_version=self._backend_version,
         )
 
     # inherited
diff --git a/narwhals/expression.py b/narwhals/expression.py
index 68fec666..10ef379f 100644
--- a/narwhals/expression.py
+++ b/narwhals/expression.py
@@ -3006,7 +3006,7 @@ def len() -> Expr:
         if (
             (pl := get_polars()) is not None
             and plx is pl
-            and parse_version(pl.__version__) < parse_version("0.20.4")
+            and parse_version(pl.__version__) < (0, 20, 4)
         ):  # pragma: no cover
             return plx.count().alias("len")
         return plx.len()
diff --git a/narwhals/group_by.py b/narwhals/group_by.py
index 1fe7c23d..b0d83b40 100644
--- a/narwhals/group_by.py
+++ b/narwhals/group_by.py
@@ -110,7 +110,7 @@ class GroupBy(Generic[DataFrameT]):
             └─────┴─────┴─────┘
         """
         aggs, named_aggs = self._df._flatten_and_extract(*aggs, **named_aggs)
-        return self._df.__class__(  # type: ignore[return-value]
+        return self._df._from_dataframe(  # type: ignore[return-value]
             self._grouped.agg(*aggs, **named_aggs),
         )
 
@@ -131,6 +131,6 @@ class LazyGroupBy(Generic[LazyFrameT]):
         self, *aggs: IntoExpr | Iterable[IntoExpr], **named_aggs: IntoExpr
     ) -> LazyFrameT:
         aggs, named_aggs = self._df._flatten_and_extract(*aggs, **named_aggs)
-        return self._df.__class__(  # type: ignore[return-value]
+        return self._df._from_dataframe(  # type: ignore[return-value]
             self._grouped.agg(*aggs, **named_aggs),
         )
diff --git a/narwhals/series.py b/narwhals/series.py
index 09d9d0d2..96905cc2 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -12,6 +12,7 @@ from narwhals.dependencies import get_polars
 from narwhals.dependencies import get_pyarrow
 from narwhals.dtypes import to_narwhals_dtype
 from narwhals.dtypes import translate_dtype
+from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
     import numpy as np
@@ -35,11 +36,13 @@ class Series:
         self,
         series: Any,
         *,
-        is_polars: bool = False,
+        backend_version: tuple[int, ...],
+        is_polars: bool,
     ) -> None:
         from narwhals._pandas_like.series import PandasSeries
 
         self._is_polars = is_polars
+        self._backend_version = backend_version
         if hasattr(series, "__narwhals_series__"):
             self._series = series.__narwhals_series__()
             return
@@ -50,22 +53,36 @@ class Series:
             self._is_polars = True
             return
         if (pd := get_pandas()) is not None and isinstance(series, pd.Series):
-            self._series = PandasSeries(series, implementation="pandas")
+            self._series = PandasSeries(
+                series,
+                implementation="pandas",
+                backend_version=parse_version(pd.__version__),
+            )
             return
-        if (pd := get_modin()) is not None and isinstance(
-            series, pd.Series
+        if (mpd := get_modin()) is not None and isinstance(
+            series, mpd.Series
         ):  # pragma: no cover
-            self._series = PandasSeries(series, implementation="modin")
+            self._series = PandasSeries(
+                series,
+                implementation="modin",
+                backend_version=parse_version(mpd.__version__),
+            )
             return
-        if (pd := get_cudf()) is not None and isinstance(
-            series, pd.Series
+        if (cudf := get_cudf()) is not None and isinstance(
+            series, cudf.Series
         ):  # pragma: no cover
-            self._series = PandasSeries(series, implementation="cudf")
+            self._series = PandasSeries(
+                series,
+                implementation="cudf",
+                backend_version=parse_version(cudf.__version__),
+            )
             return
         if (pa := get_pyarrow()) is not None and isinstance(
             series, pa.ChunkedArray
         ):  # pragma: no cover
-            self._series = ArrowSeries(series, name="")
+            self._series = ArrowSeries(
+                series, name="", backend_version=parse_version(pa.__version__)
+            )
             return
         msg = (  # pragma: no cover
             f"Expected pandas, Polars, modin, or cuDF Series, got: {type(series)}. "
@@ -129,7 +146,9 @@ class Series:
         return arg
 
     def _from_series(self, series: Any) -> Self:
-        return self.__class__(series, is_polars=self._is_polars)
+        return self.__class__(
+            series, is_polars=self._is_polars, backend_version=self._backend_version
+        )
 
     def __repr__(self) -> str:  # pragma: no cover
         header = " Narwhals Series                                 "
@@ -316,7 +335,11 @@ class Series:
         """
         from narwhals.dataframe import DataFrame
 
-        return DataFrame(self._series.to_frame())
+        return DataFrame(
+            self._series.to_frame(),
+            is_polars=self._is_polars,
+            backend_version=self._backend_version,
+        )
 
     def to_list(self) -> list[Any]:
         """
@@ -1347,7 +1370,7 @@ class Series:
                 true
             ]
         """
-        return self.__class__(self._series.is_duplicated())
+        return self._from_series(self._series.is_duplicated())
 
     def is_empty(self: Self) -> bool:
         r"""
@@ -1415,7 +1438,7 @@ class Series:
                 false
             ]
         """
-        return self.__class__(self._series.is_unique())
+        return self._from_series(self._series.is_unique())
 
     def null_count(self: Self) -> int:
         r"""
@@ -1486,7 +1509,7 @@ class Series:
                 false
             ]
         """
-        return self.__class__(self._series.is_first_distinct())
+        return self._from_series(self._series.is_first_distinct())
 
     def is_last_distinct(self: Self) -> Self:
         r"""
@@ -1526,7 +1549,7 @@ class Series:
                 true
             ]
         """
-        return self.__class__(self._series.is_last_distinct())
+        return self._from_series(self._series.is_last_distinct())
 
     def is_sorted(self: Self, *, descending: bool = False) -> bool:
         r"""
@@ -1607,7 +1630,11 @@ class Series:
         """
         from narwhals.dataframe import DataFrame
 
-        return DataFrame(self._series.value_counts(sort=sort, parallel=parallel))
+        return DataFrame(
+            self._series.value_counts(sort=sort, parallel=parallel),
+            is_polars=self._is_polars,
+            backend_version=self._backend_version,
+        )
 
     def quantile(
         self,
@@ -1910,7 +1937,7 @@ class SeriesCatNamespace:
                "mango"
             ]
         """
-        return self._series.__class__(self._series._series.cat.get_categories())
+        return self._series._from_series(self._series._series.cat.get_categories())
 
 
 class SeriesStringNamespace:
@@ -1955,7 +1982,7 @@ class SeriesStringNamespace:
                null
             ]
         """
-        return self._series.__class__(self._series._series.str.starts_with(prefix))
+        return self._series._from_series(self._series._series.str.starts_with(prefix))
 
     def ends_with(self, suffix: str) -> Series:
         r"""
@@ -1995,7 +2022,7 @@ class SeriesStringNamespace:
                null
             ]
         """
-        return self._series.__class__(self._series._series.str.ends_with(suffix))
+        return self._series._from_series(self._series._series.str.ends_with(suffix))
 
     def contains(self, pattern: str, *, literal: bool = False) -> Series:
         r"""
@@ -2041,7 +2068,7 @@ class SeriesStringNamespace:
                null
             ]
         """
-        return self._series.__class__(
+        return self._series._from_series(
             self._series._series.str.contains(pattern, literal=literal)
         )
 
@@ -2110,7 +2137,7 @@ class SeriesStringNamespace:
                 "uit"
             ]
         """
-        return self._series.__class__(
+        return self._series._from_series(
             self._series._series.str.slice(offset=offset, length=length)
         )
 
@@ -2158,7 +2185,7 @@ class SeriesStringNamespace:
                "zukky"
             ]
         """
-        return self._series.__class__(self._series._series.str.slice(0, n))
+        return self._series._from_series(self._series._series.str.slice(0, n))
 
     def tail(self, n: int = 5) -> Series:
         r"""
@@ -2204,7 +2231,7 @@ class SeriesStringNamespace:
                "kkyun"
             ]
         """
-        return self._series.__class__(self._series._series.str.slice(-n))
+        return self._series._from_series(self._series._series.str.slice(-n))
 
 
 class SeriesDateTimeNamespace:
@@ -2244,7 +2271,7 @@ class SeriesDateTimeNamespace:
                2023
             ]
         """
-        return self._series.__class__(self._series._series.dt.year())
+        return self._series._from_series(self._series._series.dt.year())
 
     def month(self) -> Series:
         """
@@ -2279,7 +2306,7 @@ class SeriesDateTimeNamespace:
                8
             ]
         """
-        return self._series.__class__(self._series._series.dt.month())
+        return self._series._from_series(self._series._series.dt.month())
 
     def day(self) -> Series:
         """
@@ -2314,7 +2341,7 @@ class SeriesDateTimeNamespace:
                5
             ]
         """
-        return self._series.__class__(self._series._series.dt.day())
+        return self._series._from_series(self._series._series.dt.day())
 
     def hour(self) -> Series:
         """
@@ -2349,7 +2376,7 @@ class SeriesDateTimeNamespace:
                9
             ]
         """
-        return self._series.__class__(self._series._series.dt.hour())
+        return self._series._from_series(self._series._series.dt.hour())
 
     def minute(self) -> Series:
         """
@@ -2384,7 +2411,7 @@ class SeriesDateTimeNamespace:
                12
             ]
         """
-        return self._series.__class__(self._series._series.dt.minute())
+        return self._series._from_series(self._series._series.dt.minute())
 
     def second(self) -> Series:
         """
@@ -2419,7 +2446,7 @@ class SeriesDateTimeNamespace:
                 4
             ]
         """
-        return self._series.__class__(self._series._series.dt.second())
+        return self._series._from_series(self._series._series.dt.second())
 
     def millisecond(self) -> Series:
         """
@@ -2467,7 +2494,7 @@ class SeriesDateTimeNamespace:
                 200
             ]
         """
-        return self._series.__class__(self._series._series.dt.millisecond())
+        return self._series._from_series(self._series._series.dt.millisecond())
 
     def microsecond(self) -> Series:
         """
@@ -2515,7 +2542,7 @@ class SeriesDateTimeNamespace:
                200000
             ]
         """
-        return self._series.__class__(self._series._series.dt.microsecond())
+        return self._series._from_series(self._series._series.dt.microsecond())
 
     def nanosecond(self) -> Series:
         """
@@ -2553,7 +2580,7 @@ class SeriesDateTimeNamespace:
                60000000
             ]
         """
-        return self._series.__class__(self._series._series.dt.nanosecond())
+        return self._series._from_series(self._series._series.dt.nanosecond())
 
     def ordinal_day(self) -> Series:
         """
@@ -2588,7 +2615,7 @@ class SeriesDateTimeNamespace:
                216
             ]
         """
-        return self._series.__class__(self._series._series.dt.ordinal_day())
+        return self._series._from_series(self._series._series.dt.ordinal_day())
 
     def total_minutes(self) -> Series:
         """
@@ -2628,7 +2655,7 @@ class SeriesDateTimeNamespace:
                     20
             ]
         """
-        return self._series.__class__(self._series._series.dt.total_minutes())
+        return self._series._from_series(self._series._series.dt.total_minutes())
 
     def total_seconds(self) -> Series:
         """
@@ -2668,7 +2695,7 @@ class SeriesDateTimeNamespace:
                     20
             ]
         """
-        return self._series.__class__(self._series._series.dt.total_seconds())
+        return self._series._from_series(self._series._series.dt.total_seconds())
 
     def total_milliseconds(self) -> Series:
         """
@@ -2711,7 +2738,7 @@ class SeriesDateTimeNamespace:
                     20
             ]
         """
-        return self._series.__class__(self._series._series.dt.total_milliseconds())
+        return self._series._from_series(self._series._series.dt.total_milliseconds())
 
     def total_microseconds(self) -> Series:
         """
@@ -2754,7 +2781,7 @@ class SeriesDateTimeNamespace:
                     1200
             ]
         """
-        return self._series.__class__(self._series._series.dt.total_microseconds())
+        return self._series._from_series(self._series._series.dt.total_microseconds())
 
     def total_nanoseconds(self) -> Series:
         """
@@ -2794,7 +2821,7 @@ class SeriesDateTimeNamespace:
                     1
             ]
         """
-        return self._series.__class__(self._series._series.dt.total_nanoseconds())
+        return self._series._from_series(self._series._series.dt.total_nanoseconds())
 
     def to_string(self, format: str) -> Series:  # noqa: A002
         """
@@ -2867,4 +2894,4 @@ class SeriesDateTimeNamespace:
                "2020/05/01"
             ]
         """
-        return self._series.__class__(self._series._series.dt.to_string(format))
+        return self._series._from_series(self._series._series.dt.to_string(format))
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 8791b082..008caca0 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -199,7 +199,7 @@ def from_native(
 ) -> Any: ...
 
 
-def from_native(
+def from_native(  # noqa: PLR0915
     native_dataframe: Any,
     *,
     strict: bool = True,
@@ -233,6 +233,7 @@ def from_native(
     from narwhals.dataframe import DataFrame
     from narwhals.dataframe import LazyFrame
     from narwhals.series import Series
+    from narwhals.utils import parse_version
 
     if series_only:
         allow_series = True
@@ -241,61 +242,130 @@ def from_native(
     if (pl := get_polars()) is not None and isinstance(native_dataframe, pl.DataFrame):
         if series_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `series_only` with polars.DataFrame")
-        return DataFrame(native_dataframe)
+        return DataFrame(
+            native_dataframe,
+            is_polars=True,
+            backend_version=parse_version(pl.__version__),
+        )
     elif (pl := get_polars()) is not None and isinstance(native_dataframe, pl.LazyFrame):
         if series_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `series_only` with polars.LazyFrame")
         if eager_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `eager_only` with polars.LazyFrame")
-        return LazyFrame(native_dataframe)
-    elif (
-        (pd := get_pandas()) is not None
-        and isinstance(native_dataframe, pd.DataFrame)
-        or (mpd := get_modin()) is not None
-        and isinstance(native_dataframe, mpd.DataFrame)
-        or (cudf := get_cudf()) is not None
-        and isinstance(native_dataframe, cudf.DataFrame)
-    ):
+        return LazyFrame(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(pl.__version__),
+        )
+    elif (pd := get_pandas()) is not None and isinstance(native_dataframe, pd.DataFrame):
         if series_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `series_only` with dataframe")
-        return DataFrame(native_dataframe)
+        return DataFrame(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(pd.__version__),
+        )
+    elif (mpd := get_modin()) is not None and isinstance(
+        native_dataframe, mpd.DataFrame
+    ):  # pragma: no cover
+        if series_only:
+            raise TypeError("Cannot only use `series_only` with modin.DataFrame")
+        return DataFrame(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(mpd.__version__),
+        )
+    elif (cudf := get_cudf()) is not None and isinstance(  # pragma: no cover
+        native_dataframe, cudf.DataFrame
+    ):
+        if series_only:
+            raise TypeError("Cannot only use `series_only` with modin.DataFrame")
+        return DataFrame(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(cudf.__version__),
+        )
     elif (pa := get_pyarrow()) is not None and isinstance(native_dataframe, pa.Table):
         if series_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `series_only` with arrow table")
-        return DataFrame(native_dataframe)
+        return DataFrame(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(pa.__version__),
+        )
     elif hasattr(native_dataframe, "__narwhals_dataframe__"):  # pragma: no cover
         if series_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `series_only` with dataframe")
-        return DataFrame(native_dataframe.__narwhals_dataframe__())
+        # placeholder (0,) version here, as we wouldn't use it in this case anyway.
+        return DataFrame(
+            native_dataframe.__narwhals_dataframe__(),
+            is_polars=False,
+            backend_version=(0,),
+        )
     elif hasattr(native_dataframe, "__narwhals_lazyframe__"):  # pragma: no cover
         if series_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `series_only` with lazyframe")
         if eager_only:  # pragma: no cover (todo)
             raise TypeError("Cannot only use `eager_only` with lazyframe")
-        return LazyFrame(native_dataframe.__narwhals_lazyframe__())
-    elif (
-        (pl := get_polars()) is not None
-        and isinstance(native_dataframe, pl.Series)
-        or (pl := get_polars()) is not None
-        and isinstance(native_dataframe, pl.Series)
-        or (
-            (pd := get_pandas()) is not None
-            and isinstance(native_dataframe, pd.Series)
-            or (mpd := get_modin()) is not None
-            and isinstance(native_dataframe, mpd.Series)
-            or (cudf := get_cudf()) is not None
-            and isinstance(native_dataframe, cudf.Series)
-            or (pa := get_pyarrow()) is not None
-            and isinstance(native_dataframe, pa.ChunkedArray)
+        # placeholder (0,) version here, as we wouldn't use it in this case anyway.
+        return LazyFrame(
+            native_dataframe.__narwhals_lazyframe__(),
+            is_polars=False,
+            backend_version=(0,),
+        )
+    elif (pl := get_polars()) is not None and isinstance(native_dataframe, pl.Series):
+        if not allow_series:  # pragma: no cover (todo)
+            raise TypeError("Please set `allow_series=True`")
+        return Series(
+            native_dataframe,
+            is_polars=True,
+            backend_version=parse_version(pl.__version__),
+        )
+    elif (pd := get_pandas()) is not None and isinstance(native_dataframe, pd.Series):
+        if not allow_series:  # pragma: no cover (todo)
+            raise TypeError("Please set `allow_series=True`")
+        return Series(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(pd.__version__),
+        )
+    elif (mpd := get_modin()) is not None and isinstance(
+        native_dataframe, mpd.Series
+    ):  # pragma: no cover
+        if not allow_series:  # pragma: no cover (todo)
+            raise TypeError("Please set `allow_series=True`")
+        return Series(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(mpd.__version__),
+        )
+    elif (cudf := get_cudf()) is not None and isinstance(
+        native_dataframe, cudf.Series
+    ):  # pragma: no cover
+        if not allow_series:  # pragma: no cover (todo)
+            raise TypeError("Please set `allow_series=True`")
+        return Series(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(cudf.__version__),
         )
+    elif (pa := get_pyarrow()) is not None and isinstance(
+        native_dataframe, pa.ChunkedArray
     ):
         if not allow_series:  # pragma: no cover (todo)
             raise TypeError("Please set `allow_series=True`")
-        return Series(native_dataframe)
+        return Series(
+            native_dataframe,
+            is_polars=False,
+            backend_version=parse_version(pa.__version__),
+        )
     elif hasattr(native_dataframe, "__narwhals_series__"):  # pragma: no cover
         if not allow_series:  # pragma: no cover (todo)
             raise TypeError("Please set `allow_series=True`")
-        return Series(native_dataframe.__narwhals_series__())
+        # placeholder (0,) version here, as we wouldn't use it in this case anyway.
+        return Series(
+            native_dataframe.__narwhals_series__(), backend_version=(0,), is_polars=False
+        )
     elif strict:  # pragma: no cover
         msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_dataframe)}"
         raise TypeError(msg)
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 8e06adf9..a974493c 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -135,8 +135,6 @@ def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T:
     """
     from narwhals._pandas_like.dataframe import PandasDataFrame
     from narwhals._pandas_like.series import PandasSeries
-    from narwhals.dataframe import DataFrame
-    from narwhals.series import Series
 
     def _validate_index(index: Any) -> None:
         if not index.is_unique:
@@ -149,7 +147,7 @@ def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T:
     ):
         _validate_index(lhs_any._dataframe._native_dataframe.index)
         _validate_index(rhs_any._dataframe._native_dataframe.index)
-        return DataFrame(  # type: ignore[return-value]
+        return lhs_any._from_dataframe(  # type: ignore[no-any-return]
             lhs_any._dataframe._from_native_dataframe(
                 lhs_any._dataframe._native_dataframe.loc[
                     rhs_any._dataframe._native_dataframe.index
@@ -161,7 +159,7 @@ def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T:
     ):
         _validate_index(lhs_any._dataframe._native_dataframe.index)
         _validate_index(rhs_any._series._native_series.index)
-        return DataFrame(  # type: ignore[return-value]
+        return lhs_any._from_dataframe(  # type: ignore[no-any-return]
             lhs_any._dataframe._from_native_dataframe(
                 lhs_any._dataframe._native_dataframe.loc[
                     rhs_any._series._native_series.index
@@ -173,7 +171,7 @@ def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T:
     ):
         _validate_index(lhs_any._series._native_series.index)
         _validate_index(rhs_any._dataframe._native_dataframe.index)
-        return Series(  # type: ignore[return-value]
+        return lhs_any._from_series(  # type: ignore[no-any-return]
             lhs_any._series._from_native_series(
                 lhs_any._series._native_series.loc[
                     rhs_any._dataframe._native_dataframe.index
@@ -185,7 +183,7 @@ def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T:
     ):
         _validate_index(lhs_any._series._native_series.index)
         _validate_index(rhs_any._series._native_series.index)
-        return Series(  # type: ignore[return-value]
+        return lhs_any._from_series(  # type: ignore[no-any-return]
             lhs_any._series._from_native_series(
                 lhs_any._series._native_series.loc[rhs_any._series._native_series.index]
             )
@@ -220,11 +218,10 @@ def maybe_set_index(df: T, column_names: str | list[str]) -> T:
         5  2
     """
     from narwhals._pandas_like.dataframe import PandasDataFrame
-    from narwhals.dataframe import DataFrame
 
     df_any = cast(Any, df)
     if isinstance(getattr(df_any, "_dataframe", None), PandasDataFrame):
-        return DataFrame(  # type: ignore[return-value]
+        return df_any._from_dataframe(  # type: ignore[no-any-return]
             df_any._dataframe._from_native_dataframe(
                 df_any._dataframe._native_dataframe.set_index(column_names)
             )
@@ -258,11 +255,10 @@ def maybe_convert_dtypes(df: T, *args: bool, **kwargs: bool | str) -> T:
         dtype: object
     """
     from narwhals._pandas_like.dataframe import PandasDataFrame
-    from narwhals.dataframe import DataFrame
 
     df_any = cast(Any, df)
     if isinstance(getattr(df_any, "_dataframe", None), PandasDataFrame):
-        return DataFrame(  # type: ignore[return-value]
+        return df_any._from_dataframe(  # type: ignore[no-any-return]
             df_any._dataframe._from_native_dataframe(
                 df_any._dataframe._native_dataframe.convert_dtypes(*args, **kwargs)
             )
diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py
index b40b55ad..1256e957 100644
--- a/utils/check_api_reference.py
+++ b/utils/check_api_reference.py
@@ -37,7 +37,7 @@ if extra := set(documented).difference(top_level_functions):
 
 top_level_functions = [
     i
-    for i in nw.DataFrame(pl.DataFrame()).__dir__()
+    for i in nw.DataFrame(pl.DataFrame(), is_polars=True, backend_version=(0,)).__dir__()
     if not i[0].isupper() and i[0] != "_"
 ]
 with open("docs/api-reference/dataframe.md") as fd:
@@ -58,7 +58,7 @@ if extra := set(documented).difference(top_level_functions):
 
 top_level_functions = [
     i
-    for i in nw.LazyFrame(pl.LazyFrame()).__dir__()
+    for i in nw.LazyFrame(pl.LazyFrame(), is_polars=True, backend_version=(0,)).__dir__()
     if not i[0].isupper() and i[0] != "_"
 ]
 with open("docs/api-reference/lazyframe.md") as fd:
@@ -78,7 +78,9 @@ if extra := set(documented).difference(top_level_functions):
     ret = 1
 
 top_level_functions = [
-    i for i in nw.Series(pl.Series()).__dir__() if not i[0].isupper() and i[0] != "_"
+    i
+    for i in nw.Series(pl.Series(), backend_version=(1,), is_polars=False).__dir__()
+    if not i[0].isupper() and i[0] != "_"
 ]
 with open("docs/api-reference/series.md") as fd:
     content = fd.read()
@@ -132,7 +134,9 @@ if extra := set(documented).difference(top_level_functions):
 # Check Expr vs Series
 expr = [i for i in nw.Expr(lambda: 0).__dir__() if not i[0].isupper() and i[0] != "_"]
 series = [
-    i for i in nw.Series(pl.Series()).__dir__() if not i[0].isupper() and i[0] != "_"
+    i
+    for i in nw.Series(pl.Series(), backend_version=(1,), is_polars=True).__dir__()
+    if not i[0].isupper() and i[0] != "_"
 ]
 
 if missing := set(expr).difference(series).difference({"over"}):
diff --git a/utils/check_backend_completeness.py b/utils/check_backend_completeness.py
index d387682a..18d2e6d9 100644
--- a/utils/check_backend_completeness.py
+++ b/utils/check_backend_completeness.py
@@ -106,8 +106,10 @@ class MockSeries:
 if __name__ == "__main__":
     missing = []
 
-    df_pa = ArrowDataFrame(MockDataFrame({"a": [1, 2, 3]}))
-    df_pd = nw.DataFrame(MockDataFrame({"a": [1, 2, 3]}), is_polars=True)
+    df_pa = ArrowDataFrame(MockDataFrame({"a": [1, 2, 3]}), backend_version=(13, 0))
+    df_pd = nw.DataFrame(
+        MockDataFrame({"a": [1, 2, 3]}), is_polars=True, backend_version=(1,)
+    )
     pa_methods = [f"DataFrame.{x}" for x in df_pa.__dir__() if not x.startswith("_")]
     pd_methods = [f"DataFrame.{x}" for x in df_pd.__dir__() if not x.startswith("_")]
     missing.extend([x for x in pd_methods if x not in pa_methods and x not in MISSING])


Test patch:
diff --git a/tests/frame/test_common.py b/tests/frame/test_common.py
index 37256ee5..a352812c 100644
--- a/tests/frame/test_common.py
+++ b/tests/frame/test_common.py
@@ -272,7 +272,7 @@ def test_lazy_instantiation_error(df_raw: Any) -> None:
     with pytest.raises(
         TypeError, match="Can't instantiate DataFrame from Polars LazyFrame."
     ):
-        _ = nw.DataFrame(df_raw).shape
+        _ = nw.DataFrame(df_raw, is_polars=True, backend_version=(0,)).shape
 
 
 @pytest.mark.parametrize("df_raw", [df_polars, df_pandas, df_mpd])
@@ -289,12 +289,12 @@ def test_accepted_dataframes() -> None:
         TypeError,
         match="Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: <class 'numpy.ndarray'>",
     ):
-        nw.DataFrame(array)
+        nw.DataFrame(array, is_polars=False, backend_version=(1,))
     with pytest.raises(
         TypeError,
         match="Expected Polars lazyframe or object that implements `__narwhals_lazyframe__`, got: <class 'numpy.ndarray'>",
     ):
-        nw.LazyFrame(array)
+        nw.LazyFrame(array, is_polars=False, backend_version=(1,))
 
 
 @pytest.mark.parametrize("df_raw", [df_polars, df_pandas, df_mpd, df_pa])