Repository: narwhals-dev/narwhals
Pull request #2764 (2025-06-30 20:24:06)

refactor: Cache, reuse `Implementation._backend_version`
## What type of PR is this? (check all applicable)

- [x] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [x] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues
- https://github.com/narwhals-dev/narwhals/pull/2620#discussion_r2140114096
- https://github.com/narwhals-dev/narwhals/discussions/1657#discussioncomment-12211098
- #2713
  - `_backend_version` is no longer specified in `Compliant*` protocols

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below
The diff is big, but the changes boil down to:

1. `Implementation._backend_version` is now cached (https://github.com/narwhals-dev/narwhals/pull/2764/commits/5fac883cf0e5d68f392fa8c09b6297a1b9ba7fff)
2. We don't store and pass around the version tuple on every class

The validation of `backend_version` occurs *before* returning the cached value.
Therefore, if we manage to access it - it must be valid - so we don't need to repeat that validation for every `DataFrame`, `LazyFrame`, `Series`.

In the cases where we might be importing a backend for the first time (`lazy|collect`), the version will either be 
1. Validated for the first time, or
2. Returned (as the validation was cached)

## Tasks
Most changes are factoring-out `_backend_version` from `Compliant*` to use `Implementation._backend_version()` instead:
- [x] `Eager*`, `PandasLike*`, `Arrow*` classes
- [x] `Lazy*`, `Dask*`, `DuckDB*`, `Ibis*`, `SparkLike*`
- [x] `Compliant*`
  - [x] Expr, Selector, When-Then
  - [x] Series
  - [x] DataFrame
  - [x] LazyFrame
  - [x] Namespace
- [x] `Polars*`
  - [x] Expr
  - [x] Series
  - [x] DataFrame
  - [x] LazyFrame
  - [x] Namespace
- [x] `narwhals/_*/utils.py`
  - [x] polars
  - [x] pandas_like
  - [x] arrow
  - [x] dask
  - [x] duckdb
  - [x] ibis
  - [x] spark_like
- [x] Clean up (https://github.com/narwhals-dev/narwhals/pull/2764/commits/879f887c1cdcbca851b6602d90a1049ee8a0b5a2), (https://github.com/narwhals-dev/narwhals/pull/2764/commits/8d8bbe68fee3c174a21fed451c7564db11be09ba)
- [x] Investigate coverage (split into other PRs)
  - [x] #2770
  - [x] #2771


Patch:
diff --git a/docs/how_it_works.md b/docs/how_it_works.md
index cfa02528..add65aac 100644
--- a/docs/how_it_works.md
+++ b/docs/how_it_works.md
@@ -74,7 +74,6 @@ from narwhals._utils import parse_version, Version
 
 pn = PandasLikeNamespace(
     implementation=Implementation.PANDAS,
-    backend_version=parse_version(pd.__version__),
     version=Version.MAIN,
 )
 print(nw.col("a")._to_compliant_expr(pn))
@@ -101,7 +100,6 @@ import pandas as pd
 
 pn = PandasLikeNamespace(
     implementation=Implementation.PANDAS,
-    backend_version=parse_version(pd.__version__),
     version=Version.MAIN,
 )
 
@@ -109,7 +107,6 @@ df_pd = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
 df = PandasLikeDataFrame(
     df_pd,
     implementation=Implementation.PANDAS,
-    backend_version=parse_version(pd.__version__),
     version=Version.MAIN,
     validate_column_names=True,
 )
@@ -199,7 +196,6 @@ import pandas as pd
 
 pn = PandasLikeNamespace(
     implementation=Implementation.PANDAS,
-    backend_version=parse_version(pd.__version__),
     version=Version.MAIN,
 )
 
@@ -214,7 +210,6 @@ backend, and it does so by passing a Narwhals-compliant namespace to `nw.Expr._t
 ```python exec="1" result="python" session="pandas_api_mapping" source="above"
 pn = PandasLikeNamespace(
     implementation=Implementation.PANDAS,
-    backend_version=parse_version(pd.__version__),
     version=Version.MAIN,
 )
 expr = (nw.col("a") + 1)._to_compliant_expr(pn)
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 9d2c8479..120d11f3 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -19,10 +19,8 @@ from narwhals._utils import (
     generate_temporary_column_name,
     not_implemented,
     parse_columns_to_drop,
-    parse_version,
     scale_bytes,
     supports_arrow_c_stream,
-    validate_backend_version,
 )
 from narwhals.dependencies import is_numpy_array_1d
 from narwhals.exceptions import ShapeError
@@ -46,7 +44,7 @@ if TYPE_CHECKING:
     )
     from narwhals._compliant.typing import CompliantDataFrameAny, CompliantLazyFrameAny
     from narwhals._translate import IntoArrowTable
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.dtypes import DType
     from narwhals.schema import Schema
     from narwhals.typing import (
@@ -77,25 +75,26 @@ if TYPE_CHECKING:
 class ArrowDataFrame(
     EagerDataFrame["ArrowSeries", "ArrowExpr", "pa.Table", "ChunkedArrayAny"]
 ):
+    _implementation = Implementation.PYARROW
+
     def __init__(
         self,
         native_dataframe: pa.Table,
         *,
-        backend_version: tuple[int, ...],
         version: Version,
         validate_column_names: bool,
+        validate_backend_version: bool = False,
     ) -> None:
         if validate_column_names:
             check_column_names_are_unique(native_dataframe.column_names)
+        if validate_backend_version:
+            self._validate_backend_version()
         self._native_frame = native_dataframe
-        self._implementation = Implementation.PYARROW
-        self._backend_version = backend_version
         self._version = version
-        validate_backend_version(self._implementation, self._backend_version)
 
     @classmethod
-    def from_arrow(cls, data: IntoArrowTable, /, *, context: _FullContext) -> Self:
-        backend_version = context._backend_version
+    def from_arrow(cls, data: IntoArrowTable, /, *, context: _LimitedContext) -> Self:
+        backend_version = context._implementation._backend_version()
         if cls._is_native(data):
             native = data
         elif backend_version >= (14,) or isinstance(data, Collection):
@@ -114,7 +113,7 @@ class ArrowDataFrame(
         data: Mapping[str, Any],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         schema: Mapping[str, DType] | Schema | None,
     ) -> Self:
         from narwhals.schema import Schema
@@ -128,13 +127,8 @@ class ArrowDataFrame(
         return isinstance(obj, pa.Table)
 
     @classmethod
-    def from_native(cls, data: pa.Table, /, *, context: _FullContext) -> Self:
-        return cls(
-            data,
-            backend_version=context._backend_version,
-            version=context._version,
-            validate_column_names=True,
-        )
+    def from_native(cls, data: pa.Table, /, *, context: _LimitedContext) -> Self:
+        return cls(data, version=context._version, validate_column_names=True)
 
     @classmethod
     def from_numpy(
@@ -142,7 +136,7 @@ class ArrowDataFrame(
         data: _2DArray,
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         schema: Mapping[str, DType] | Schema | Sequence[str] | None,
     ) -> Self:
         from narwhals.schema import Schema
@@ -157,9 +151,7 @@ class ArrowDataFrame(
     def __narwhals_namespace__(self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
-        return ArrowNamespace(
-            backend_version=self._backend_version, version=self._version
-        )
+        return ArrowNamespace(version=self._version)
 
     def __native_namespace__(self) -> ModuleType:
         if self._implementation is Implementation.PYARROW:
@@ -175,19 +167,11 @@ class ArrowDataFrame(
         return self
 
     def _with_version(self, version: Version) -> Self:
-        return self.__class__(
-            self.native,
-            backend_version=self._backend_version,
-            version=version,
-            validate_column_names=False,
-        )
+        return self.__class__(self.native, version=version, validate_column_names=False)
 
     def _with_native(self, df: pa.Table, *, validate_column_names: bool = True) -> Self:
         return self.__class__(
-            df,
-            backend_version=self._backend_version,
-            version=self._version,
-            validate_column_names=validate_column_names,
+            df, version=self._version, validate_column_names=validate_column_names
         )
 
     @property
@@ -536,9 +520,7 @@ class ArrowDataFrame(
 
             df = self.native  # noqa: F841
             return DuckDBLazyFrame(
-                duckdb.table("df"),
-                backend_version=parse_version(duckdb),
-                version=self._version,
+                duckdb.table("df"), validate_backend_version=True, version=self._version
             )
         elif backend is Implementation.POLARS:
             import polars as pl  # ignore-banned-import
@@ -547,18 +529,17 @@ class ArrowDataFrame(
 
             return PolarsLazyFrame(
                 cast("pl.DataFrame", pl.from_arrow(self.native)).lazy(),
-                backend_version=parse_version(pl),
+                validate_backend_version=True,
                 version=self._version,
             )
         elif backend is Implementation.DASK:
-            import dask  # ignore-banned-import
             import dask.dataframe as dd  # ignore-banned-import
 
             from narwhals._dask.dataframe import DaskLazyFrame
 
             return DaskLazyFrame(
                 dd.from_pandas(self.native.to_pandas()),
-                backend_version=parse_version(dask),
+                validate_backend_version=True,
                 version=self._version,
             )
         elif backend.is_ibis():
@@ -568,9 +549,10 @@ class ArrowDataFrame(
 
             return IbisLazyFrame(
                 ibis.memtable(self.native, columns=self.columns),
-                backend_version=parse_version(ibis),
+                validate_backend_version=True,
                 version=self._version,
             )
+
         raise AssertionError  # pragma: no cover
 
     def collect(
@@ -580,21 +562,16 @@ class ArrowDataFrame(
             from narwhals._arrow.dataframe import ArrowDataFrame
 
             return ArrowDataFrame(
-                self.native,
-                backend_version=self._backend_version,
-                version=self._version,
-                validate_column_names=False,
+                self.native, version=self._version, validate_column_names=False
             )
 
         if backend is Implementation.PANDAS:
-            import pandas as pd  # ignore-banned-import
-
             from narwhals._pandas_like.dataframe import PandasLikeDataFrame
 
             return PandasLikeDataFrame(
                 self.native.to_pandas(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=False,
             )
@@ -606,7 +583,7 @@ class ArrowDataFrame(
 
             return PolarsDataFrame(
                 cast("pl.DataFrame", pl.from_arrow(self.native)),
-                backend_version=parse_version(pl),
+                validate_backend_version=True,
                 version=self._version,
             )
 
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 2ff68a18..324a1608 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
     from narwhals._arrow.namespace import ArrowNamespace
     from narwhals._compliant.typing import AliasNames, EvalNames, EvalSeries, ScalarKwargs
     from narwhals._expression_parsing import ExprMetadata
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
 
 
 class ArrowExpr(EagerExpr["ArrowDataFrame", ArrowSeries]):
@@ -36,7 +36,6 @@ class ArrowExpr(EagerExpr["ArrowDataFrame", ArrowSeries]):
         function_name: str,
         evaluate_output_names: EvalNames[ArrowDataFrame],
         alias_output_names: AliasNames | None,
-        backend_version: tuple[int, ...],
         version: Version,
         scalar_kwargs: ScalarKwargs | None = None,
         implementation: Implementation | None = None,
@@ -47,7 +46,6 @@ class ArrowExpr(EagerExpr["ArrowDataFrame", ArrowSeries]):
         self._depth = depth
         self._evaluate_output_names = evaluate_output_names
         self._alias_output_names = alias_output_names
-        self._backend_version = backend_version
         self._version = version
         self._scalar_kwargs = scalar_kwargs or {}
         self._metadata: ExprMetadata | None = None
@@ -58,17 +56,14 @@ class ArrowExpr(EagerExpr["ArrowDataFrame", ArrowSeries]):
         evaluate_column_names: EvalNames[ArrowDataFrame],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         function_name: str = "",
     ) -> Self:
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
             try:
                 return [
                     ArrowSeries(
-                        df.native[column_name],
-                        name=column_name,
-                        backend_version=df._backend_version,
-                        version=df._version,
+                        df.native[column_name], name=column_name, version=df._version
                     )
                     for column_name in evaluate_column_names(df)
                 ]
@@ -83,12 +78,11 @@ class ArrowExpr(EagerExpr["ArrowDataFrame", ArrowSeries]):
             function_name=function_name,
             evaluate_output_names=evaluate_column_names,
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
     @classmethod
-    def from_column_indices(cls, *column_indices: int, context: _FullContext) -> Self:
+    def from_column_indices(cls, *column_indices: int, context: _LimitedContext) -> Self:
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
             tbl = df.native
             cols = df.columns
@@ -103,16 +97,13 @@ class ArrowExpr(EagerExpr["ArrowDataFrame", ArrowSeries]):
             function_name="nth",
             evaluate_output_names=cls._eval_names_indices(column_indices),
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
     def __narwhals_namespace__(self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
-        return ArrowNamespace(
-            backend_version=self._backend_version, version=self._version
-        )
+        return ArrowNamespace(version=self._version)
 
     def __narwhals_expr__(self) -> None: ...
 
@@ -175,7 +166,6 @@ class ArrowExpr(EagerExpr["ArrowDataFrame", ArrowSeries]):
             function_name=self._function_name + "->over",
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index 82cc37a7..2faa2d5b 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -31,6 +31,8 @@ if TYPE_CHECKING:
 class ArrowNamespace(
     EagerNamespace[ArrowDataFrame, ArrowSeries, ArrowExpr, pa.Table, "ChunkedArrayAny"]
 ):
+    _implementation = Implementation.PYARROW
+
     @property
     def _dataframe(self) -> type[ArrowDataFrame]:
         return ArrowDataFrame
@@ -43,10 +45,7 @@ class ArrowNamespace(
     def _series(self) -> type[ArrowSeries]:
         return ArrowSeries
 
-    # --- not in spec ---
-    def __init__(self, *, backend_version: tuple[int, ...], version: Version) -> None:
-        self._backend_version = backend_version
-        self._implementation = Implementation.PYARROW
+    def __init__(self, *, version: Version) -> None:
         self._version = version
 
     def len(self) -> ArrowExpr:
@@ -59,7 +58,6 @@ class ArrowNamespace(
             function_name="len",
             evaluate_output_names=lambda _df: ["len"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -78,7 +76,6 @@ class ArrowNamespace(
             function_name="lit",
             evaluate_output_names=lambda _df: ["literal"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -168,12 +165,7 @@ class ArrowNamespace(
                 pc.min_element_wise, [s.native for s in series], init_series.native
             )
             return [
-                ArrowSeries(
-                    native_series,
-                    name=init_series.name,
-                    backend_version=self._backend_version,
-                    version=self._version,
-                )
+                ArrowSeries(native_series, name=init_series.name, version=self._version)
             ]
 
         return self._expr._from_callable(
@@ -194,12 +186,7 @@ class ArrowNamespace(
                 pc.max_element_wise, [s.native for s in series], init_series.native
             )
             return [
-                ArrowSeries(
-                    native_series,
-                    name=init_series.name,
-                    backend_version=self._backend_version,
-                    version=self._version,
-                )
+                ArrowSeries(native_series, name=init_series.name, version=self._version)
             ]
 
         return self._expr._from_callable(
@@ -262,7 +249,6 @@ class ArrowNamespace(
             compliant = self._series(
                 concat_str(*it, separator_scalar, null_handling=null_handling),
                 name=name,
-                backend_version=self._backend_version,
                 version=self._version,
             )
             return [compliant]
diff --git a/narwhals/_arrow/selectors.py b/narwhals/_arrow/selectors.py
index d72da05c..cd42d141 100644
--- a/narwhals/_arrow/selectors.py
+++ b/narwhals/_arrow/selectors.py
@@ -24,6 +24,5 @@ class ArrowSelector(CompliantSelector["ArrowDataFrame", "ArrowSeries"], ArrowExp
             function_name=self._function_name,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 621cb9ea..58d5e5fa 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -30,7 +30,6 @@ from narwhals._utils import (
     is_list_of,
     not_implemented,
     requires,
-    validate_backend_version,
 )
 from narwhals.dependencies import is_numpy_array_1d
 from narwhals.exceptions import InvalidOperationError, ShapeError
@@ -57,7 +56,7 @@ if TYPE_CHECKING:
         _AsPyType,
         _BasicDataType,
     )
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.dtypes import DType
     from narwhals.typing import (
         ClosedInterval,
@@ -116,20 +115,14 @@ def maybe_extract_py_scalar(value: Any, return_py_scalar: bool) -> Any:  # noqa:
 
 
 class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
+    _implementation = Implementation.PYARROW
+
     def __init__(
-        self,
-        native_series: ChunkedArrayAny,
-        *,
-        name: str,
-        backend_version: tuple[int, ...],
-        version: Version,
+        self, native_series: ChunkedArrayAny, *, name: str, version: Version
     ) -> None:
         self._name = name
         self._native_series: ChunkedArrayAny = native_series
-        self._implementation = Implementation.PYARROW
-        self._backend_version = backend_version
         self._version = version
-        validate_backend_version(self._implementation, self._backend_version)
         self._broadcast = False
 
     @property
@@ -137,12 +130,7 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         return self._native_series
 
     def _with_version(self, version: Version) -> Self:
-        return self.__class__(
-            self.native,
-            name=self._name,
-            backend_version=self._backend_version,
-            version=version,
-        )
+        return self.__class__(self.native, name=self._name, version=version)
 
     def _with_native(
         self, series: ArrayOrScalar, *, preserve_broadcast: bool = False
@@ -157,7 +145,7 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         cls,
         data: Iterable[Any],
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         name: str = "",
         dtype: IntoDType | None = None,
     ) -> Self:
@@ -178,17 +166,12 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
 
     @classmethod
     def from_native(
-        cls, data: ChunkedArrayAny, /, *, context: _FullContext, name: str = ""
+        cls, data: ChunkedArrayAny, /, *, context: _LimitedContext, name: str = ""
     ) -> Self:
-        return cls(
-            data,
-            backend_version=context._backend_version,
-            version=context._version,
-            name=name,
-        )
+        return cls(data, version=context._version, name=name)
 
     @classmethod
-    def from_numpy(cls, data: Into1DArray, /, *, context: _FullContext) -> Self:
+    def from_numpy(cls, data: Into1DArray, /, *, context: _LimitedContext) -> Self:
         return cls.from_iterable(
             data if is_numpy_array_1d(data) else [data], context=context
         )
@@ -215,9 +198,7 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
     def __narwhals_namespace__(self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
-        return ArrowNamespace(
-            backend_version=self._backend_version, version=self._version
-        )
+        return ArrowNamespace(version=self._version)
 
     def __eq__(self, other: object) -> Self:  # type: ignore[override]
         other = cast("PythonLiteral | ArrowSeries | None", other)
@@ -501,12 +482,7 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         return self.native.to_numpy()
 
     def alias(self, name: str) -> Self:
-        result = self.__class__(
-            self.native,
-            name=name,
-            backend_version=self._backend_version,
-            version=self._version,
-        )
+        result = self.__class__(self.native, name=name, version=self._version)
         result._broadcast = self._broadcast
         return result
 
@@ -644,10 +620,7 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
             val_count = val_count.sort_by([(value_name_, "descending")])
 
         return ArrowDataFrame(
-            val_count,
-            backend_version=self._backend_version,
-            version=self._version,
-            validate_column_names=True,
+            val_count, version=self._version, validate_column_names=True
         )
 
     def zip_with(self, mask: Self, other: Self) -> Self:
@@ -719,12 +692,7 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         from narwhals._arrow.dataframe import ArrowDataFrame
 
         df = pa.Table.from_arrays([self.native], names=[self.name])
-        return ArrowDataFrame(
-            df,
-            backend_version=self._backend_version,
-            version=self._version,
-            validate_column_names=False,
-        )
+        return ArrowDataFrame(df, version=self._version, validate_column_names=False)
 
     def to_pandas(self) -> pd.Series[Any]:
         import pandas as pd  # ignore-banned-import()
@@ -842,7 +810,6 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         )
         return ArrowDataFrame(
             pa.Table.from_arrays(columns, names=cols),
-            backend_version=self._backend_version,
             version=self._version,
             validate_column_names=True,
         ).simple_select(*output_order)
@@ -1151,10 +1118,7 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
         data["count"] = counts
 
         return ArrowDataFrame(
-            pa.Table.from_pydict(data),
-            backend_version=self._backend_version,
-            version=self._version,
-            validate_column_names=True,
+            pa.Table.from_pydict(data), version=self._version, validate_column_names=True
         )
 
     def __iter__(self) -> Iterator[Any]:
diff --git a/narwhals/_arrow/series_str.py b/narwhals/_arrow/series_str.py
index 0d73022e..b20623d7 100644
--- a/narwhals/_arrow/series_str.py
+++ b/narwhals/_arrow/series_str.py
@@ -72,9 +72,7 @@ class ArrowSeriesStringNamespace(ArrowSeriesNamespace):
         hyphen, plus = lit("-"), lit("+")
 
         _slice_length: int | None = (
-            self.len_chars().max()
-            if self._compliant_series._backend_version < (13, 0)
-            else None
+            self.len_chars().max() if self.backend_version < (13, 0) else None
         )
         first_char, remaining_chars = (
             self.slice(0, 1).native,
diff --git a/narwhals/_compliant/dataframe.py b/narwhals/_compliant/dataframe.py
index 7ffc044d..6199126c 100644
--- a/narwhals/_compliant/dataframe.py
+++ b/narwhals/_compliant/dataframe.py
@@ -25,6 +25,7 @@ from narwhals._translate import (
 )
 from narwhals._typing_compat import assert_never, deprecated
 from narwhals._utils import (
+    ValidateBackendVersion,
     Version,
     _StoresNative,
     check_columns_exist,
@@ -51,7 +52,7 @@ if TYPE_CHECKING:
     from narwhals._compliant.namespace import EagerNamespace
     from narwhals._compliant.window import WindowInputs
     from narwhals._translate import IntoArrowTable
-    from narwhals._utils import Implementation, _FullContext
+    from narwhals._utils import Implementation, _LimitedContext
     from narwhals.dataframe import DataFrame
     from narwhals.dtypes import DType
     from narwhals.exceptions import ColumnNotFoundError
@@ -94,31 +95,30 @@ class CompliantDataFrame(
 ):
     _native_frame: NativeFrameT
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
 
     def __narwhals_dataframe__(self) -> Self: ...
     def __narwhals_namespace__(self) -> Any: ...
     @classmethod
-    def from_arrow(cls, data: IntoArrowTable, /, *, context: _FullContext) -> Self: ...
+    def from_arrow(cls, data: IntoArrowTable, /, *, context: _LimitedContext) -> Self: ...
     @classmethod
     def from_dict(
         cls,
         data: Mapping[str, Any],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         schema: Mapping[str, DType] | Schema | None,
     ) -> Self: ...
     @classmethod
-    def from_native(cls, data: NativeFrameT, /, *, context: _FullContext) -> Self: ...
+    def from_native(cls, data: NativeFrameT, /, *, context: _LimitedContext) -> Self: ...
     @classmethod
     def from_numpy(
         cls,
         data: _2DArray,
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         schema: Mapping[str, DType] | Schema | Sequence[str] | None,
     ) -> Self: ...
 
@@ -277,14 +277,13 @@ class CompliantLazyFrame(
 ):
     _native_frame: NativeFrameT
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
 
     def __narwhals_lazyframe__(self) -> Self: ...
     def __narwhals_namespace__(self) -> Any: ...
 
     @classmethod
-    def from_native(cls, data: NativeFrameT, /, *, context: _FullContext) -> Self: ...
+    def from_native(cls, data: NativeFrameT, /, *, context: _LimitedContext) -> Self: ...
 
     def simple_select(self, *column_names: str) -> Self:
         """`select` where all args are column names."""
@@ -392,8 +391,13 @@ class CompliantLazyFrame(
 class EagerDataFrame(
     CompliantDataFrame[EagerSeriesT, EagerExprT, NativeFrameT, "DataFrame[NativeFrameT]"],
     CompliantLazyFrame[EagerExprT, NativeFrameT, "DataFrame[NativeFrameT]"],
+    ValidateBackendVersion,
     Protocol[EagerSeriesT, EagerExprT, NativeFrameT, NativeSeriesT],
 ):
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
+
     def __narwhals_namespace__(
         self,
     ) -> EagerNamespace[Self, EagerSeriesT, EagerExprT, NativeFrameT, NativeSeriesT]: ...
diff --git a/narwhals/_compliant/expr.py b/narwhals/_compliant/expr.py
index 8d0645b9..487d22f2 100644
--- a/narwhals/_compliant/expr.py
+++ b/narwhals/_compliant/expr.py
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
         WindowFunction,
     )
     from narwhals._expression_parsing import ExprKind, ExprMetadata
-    from narwhals._utils import Implementation, Version, _FullContext
+    from narwhals._utils import Implementation, Version, _LimitedContext
     from narwhals.typing import (
         FillNullStrategy,
         IntoDType,
@@ -74,7 +74,6 @@ class NativeExpr(Protocol):
 
 class CompliantExpr(Protocol38[CompliantFrameT, CompliantSeriesOrNativeExprT_co]):
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
     _evaluate_output_names: EvalNames[CompliantFrameT]
     _alias_output_names: AliasNames | None
@@ -91,10 +90,12 @@ class CompliantExpr(Protocol38[CompliantFrameT, CompliantSeriesOrNativeExprT_co]
         evaluate_column_names: EvalNames[CompliantFrameT],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
     ) -> Self: ...
     @classmethod
-    def from_column_indices(cls, *column_indices: int, context: _FullContext) -> Self: ...
+    def from_column_indices(
+        cls, *column_indices: int, context: _LimitedContext
+    ) -> Self: ...
     @staticmethod
     def _eval_names_indices(indices: Sequence[int], /) -> EvalNames[CompliantFrameT]:
         def fn(df: CompliantFrameT) -> Sequence[str]:
@@ -280,7 +281,7 @@ class DepthTrackingExpr(
         evaluate_column_names: EvalNames[CompliantFrameT],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         function_name: str = "",
     ) -> Self: ...
 
@@ -321,7 +322,6 @@ class EagerExpr(
         evaluate_output_names: EvalNames[EagerDataFrameT],
         alias_output_names: AliasNames | None,
         implementation: Implementation,
-        backend_version: tuple[int, ...],
         version: Version,
         scalar_kwargs: ScalarKwargs | None = None,
     ) -> None: ...
@@ -343,7 +343,7 @@ class EagerExpr(
         function_name: str,
         evaluate_output_names: EvalNames[EagerDataFrameT],
         alias_output_names: AliasNames | None,
-        context: _FullContext,
+        context: _LimitedContext,
         scalar_kwargs: ScalarKwargs | None = None,
     ) -> Self:
         return cls(
@@ -353,7 +353,6 @@ class EagerExpr(
             evaluate_output_names=evaluate_output_names,
             alias_output_names=alias_output_names,
             implementation=context._implementation,
-            backend_version=context._backend_version,
             version=context._version,
             scalar_kwargs=scalar_kwargs,
         )
@@ -367,7 +366,6 @@ class EagerExpr(
             evaluate_output_names=lambda _df: [series.name],
             alias_output_names=None,
             implementation=series._implementation,
-            backend_version=series._backend_version,
             version=series._version,
         )
 
@@ -502,7 +500,6 @@ class EagerExpr(
             function_name=self._function_name,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             implementation=self._implementation,
             version=self._version,
             scalar_kwargs=self._scalar_kwargs,
@@ -719,7 +716,6 @@ class EagerExpr(
             function_name=self._function_name,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=alias_output_names,
-            backend_version=self._backend_version,
             implementation=self._implementation,
             version=self._version,
             scalar_kwargs=self._scalar_kwargs,
@@ -915,6 +911,10 @@ class LazyExpr(
     replace_strict: not_implemented = not_implemented()
     cat: not_implemented = not_implemented()  # type: ignore[assignment]
 
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
+
     @property
     def window_function(self) -> WindowFunction[CompliantLazyFrameT, NativeExprT]: ...
 
@@ -1171,7 +1171,6 @@ class EagerExprNameNamespace(
             function_name=expr._function_name,
             evaluate_output_names=expr._evaluate_output_names,
             alias_output_names=self._alias_output_names(func) if alias else None,
-            backend_version=expr._backend_version,
             implementation=expr._implementation,
             version=expr._version,
             scalar_kwargs=expr._scalar_kwargs,
diff --git a/narwhals/_compliant/namespace.py b/narwhals/_compliant/namespace.py
index 0bc1f988..da3e6ea1 100644
--- a/narwhals/_compliant/namespace.py
+++ b/narwhals/_compliant/namespace.py
@@ -53,7 +53,6 @@ __all__ = [
 
 class CompliantNamespace(Protocol[CompliantFrameT, CompliantExprT]):
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
 
     def all(self) -> CompliantExprT:
@@ -125,6 +124,10 @@ class LazyNamespace(
     CompliantNamespace[CompliantLazyFrameT, LazyExprT],
     Protocol[CompliantLazyFrameT, LazyExprT, NativeFrameT_co],
 ):
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
+
     @property
     def _lazyframe(self) -> type[CompliantLazyFrameT]: ...
 
@@ -140,6 +143,10 @@ class EagerNamespace(
     DepthTrackingNamespace[EagerDataFrameT, EagerExprT],
     Protocol[EagerDataFrameT, EagerSeriesT, EagerExprT, NativeFrameT, NativeSeriesT],
 ):
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
+
     @property
     def _dataframe(self) -> type[EagerDataFrameT]: ...
     @property
diff --git a/narwhals/_compliant/selectors.py b/narwhals/_compliant/selectors.py
index 6dae7399..84e4e7ce 100644
--- a/narwhals/_compliant/selectors.py
+++ b/narwhals/_compliant/selectors.py
@@ -33,7 +33,7 @@ if TYPE_CHECKING:
         EvalSeries,
         ScalarKwargs,
     )
-    from narwhals._utils import Implementation, Version, _FullContext
+    from narwhals._utils import Implementation, Version, _LimitedContext
     from narwhals.dtypes import DType
     from narwhals.typing import TimeUnit
 
@@ -58,14 +58,12 @@ SelectorOrExpr: TypeAlias = (
 
 class CompliantSelectorNamespace(Protocol[FrameT, SeriesOrExprT]):
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
 
     @classmethod
-    def from_namespace(cls, context: _FullContext, /) -> Self:
+    def from_namespace(cls, context: _LimitedContext, /) -> Self:
         obj = cls.__new__(cls)
         obj._implementation = context._implementation
-        obj._backend_version = context._backend_version
         obj._version = context._version
         return obj
 
@@ -207,7 +205,6 @@ class CompliantSelector(
     _function_name: str
     _depth: int
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
     _scalar_kwargs: ScalarKwargs
 
@@ -217,7 +214,7 @@ class CompliantSelector(
         call: EvalSeries[FrameT, SeriesOrExprT],
         evaluate_output_names: EvalNames[FrameT],
         *,
-        context: _FullContext,
+        context: _LimitedContext,
     ) -> Self:
         obj = cls.__new__(cls)
         obj._call = call
@@ -227,7 +224,6 @@ class CompliantSelector(
         obj._evaluate_output_names = evaluate_output_names
         obj._alias_output_names = None
         obj._implementation = context._implementation
-        obj._backend_version = context._backend_version
         obj._version = context._version
         obj._scalar_kwargs = {}
         return obj
diff --git a/narwhals/_compliant/series.py b/narwhals/_compliant/series.py
index 7157011a..2db8cb96 100644
--- a/narwhals/_compliant/series.py
+++ b/narwhals/_compliant/series.py
@@ -37,7 +37,7 @@ if TYPE_CHECKING:
     from narwhals._compliant.dataframe import CompliantDataFrame
     from narwhals._compliant.expr import CompliantExpr, EagerExpr
     from narwhals._compliant.namespace import CompliantNamespace, EagerNamespace
-    from narwhals._utils import Implementation, Version, _FullContext
+    from narwhals._utils import Implementation, Version, _LimitedContext
     from narwhals.dtypes import DType
     from narwhals.series import Series
     from narwhals.typing import (
@@ -76,7 +76,6 @@ class CompliantSeries(
     Protocol[NativeSeriesT],
 ):
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
 
     @property
@@ -101,16 +100,16 @@ class CompliantSeries(
     def _with_version(self, version: Version) -> Self: ...
     def _to_expr(self) -> CompliantExpr[Any, Self]: ...
     @classmethod
-    def from_native(cls, data: NativeSeriesT, /, *, context: _FullContext) -> Self: ...
+    def from_native(cls, data: NativeSeriesT, /, *, context: _LimitedContext) -> Self: ...
     @classmethod
-    def from_numpy(cls, data: Into1DArray, /, *, context: _FullContext) -> Self: ...
+    def from_numpy(cls, data: Into1DArray, /, *, context: _LimitedContext) -> Self: ...
     @classmethod
     def from_iterable(
         cls,
         data: Iterable[Any],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         name: str = "",
         dtype: IntoDType | None = None,
     ) -> Self: ...
@@ -286,10 +285,13 @@ class CompliantSeries(
 class EagerSeries(CompliantSeries[NativeSeriesT], Protocol[NativeSeriesT]):
     _native_series: Any
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
     _broadcast: bool
 
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
+
     @classmethod
     def _align_full_broadcast(cls, *series: Self) -> Sequence[Self]:
         """Ensure all of `series` have the same length (and index if `pandas`).
@@ -366,6 +368,18 @@ class _SeriesNamespace(  # type: ignore[misc]
     def compliant(self) -> CompliantSeriesT_co:
         return self._compliant_series
 
+    @property
+    def implementation(self) -> Implementation:
+        return self.compliant._implementation
+
+    @property
+    def backend_version(self) -> tuple[int, ...]:
+        return self.implementation._backend_version()
+
+    @property
+    def version(self) -> Version:
+        return self.compliant._version
+
     @property
     def native(self) -> NativeSeriesT_co:
         return self._compliant_series.native  # type: ignore[no-any-return]
diff --git a/narwhals/_compliant/when_then.py b/narwhals/_compliant/when_then.py
index 919f4866..00ec5411 100644
--- a/narwhals/_compliant/when_then.py
+++ b/narwhals/_compliant/when_then.py
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
 
     from narwhals._compliant.typing import EvalSeries, ScalarKwargs
     from narwhals._compliant.window import WindowInputs
-    from narwhals._utils import Implementation, Version, _FullContext
+    from narwhals._utils import Implementation, Version, _LimitedContext
     from narwhals.typing import NonNestedLiteral
 
 
@@ -48,7 +48,6 @@ class CompliantWhen(Protocol38[FrameT, SeriesT, ExprT]):
     _then_value: IntoExpr[SeriesT, ExprT]
     _otherwise_value: IntoExpr[SeriesT, ExprT] | None
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
 
     @property
@@ -64,13 +63,12 @@ class CompliantWhen(Protocol38[FrameT, SeriesT, ExprT]):
         return self._then.from_when(self, value)
 
     @classmethod
-    def from_expr(cls, condition: ExprT, /, *, context: _FullContext) -> Self:
+    def from_expr(cls, condition: ExprT, /, *, context: _LimitedContext) -> Self:
         obj = cls.__new__(cls)
         obj._condition = condition
         obj._then_value = None
         obj._otherwise_value = None
         obj._implementation = context._implementation
-        obj._backend_version = context._backend_version
         obj._version = context._version
         return obj
 
@@ -81,7 +79,6 @@ class CompliantThen(CompliantExpr[FrameT, SeriesT], Protocol38[FrameT, SeriesT,
     _function_name: str
     _depth: int
     _implementation: Implementation
-    _backend_version: tuple[int, ...]
     _version: Version
     _scalar_kwargs: ScalarKwargs
 
@@ -103,7 +100,6 @@ class CompliantThen(CompliantExpr[FrameT, SeriesT], Protocol38[FrameT, SeriesT,
         )
         obj._alias_output_names = getattr(then, "_alias_output_names", None)
         obj._implementation = when._implementation
-        obj._backend_version = when._backend_version
         obj._version = when._version
         obj._scalar_kwargs = {}
         return obj
@@ -130,9 +126,7 @@ class LazyThen(
         when._then_value = then
         obj = cls.__new__(cls)
         obj._call = when
-
         obj._window_function = when._window_function
-
         obj._when_value = when
         obj._depth = 0
         obj._function_name = "whenthen"
@@ -141,7 +135,6 @@ class LazyThen(
         )
         obj._alias_output_names = getattr(then, "_alias_output_names", None)
         obj._implementation = when._implementation
-        obj._backend_version = when._backend_version
         obj._version = when._version
         obj._scalar_kwargs = {}
         return obj
@@ -204,14 +197,12 @@ class LazyWhen(
         return [result]
 
     @classmethod
-    def from_expr(cls, condition: LazyExprT, /, *, context: _FullContext) -> Self:
+    def from_expr(cls, condition: LazyExprT, /, *, context: _LimitedContext) -> Self:
         obj = cls.__new__(cls)
         obj._condition = condition
-
         obj._then_value = None
         obj._otherwise_value = None
         obj._implementation = context._implementation
-        obj._backend_version = context._backend_version
         obj._version = context._version
         return obj
 
diff --git a/narwhals/_dask/dataframe.py b/narwhals/_dask/dataframe.py
index b517474a..8928a4da 100644
--- a/narwhals/_dask/dataframe.py
+++ b/narwhals/_dask/dataframe.py
@@ -3,7 +3,6 @@ from __future__ import annotations
 from typing import TYPE_CHECKING, Any
 
 import dask.dataframe as dd
-import pandas as pd
 
 from narwhals._dask.utils import add_row_index, evaluate_exprs
 from narwhals._expression_parsing import ExprKind
@@ -11,13 +10,12 @@ from narwhals._pandas_like.utils import native_to_narwhals_dtype, select_columns
 from narwhals._typing_compat import assert_never
 from narwhals._utils import (
     Implementation,
+    ValidateBackendVersion,
     _remap_full_join_keys,
     check_column_names_are_unique,
     generate_temporary_column_name,
     not_implemented,
     parse_columns_to_drop,
-    parse_version,
-    validate_backend_version,
 )
 from narwhals.typing import CompliantLazyFrame
 
@@ -32,7 +30,7 @@ if TYPE_CHECKING:
     from narwhals._dask.expr import DaskExpr
     from narwhals._dask.group_by import DaskLazyGroupBy
     from narwhals._dask.namespace import DaskNamespace
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.dataframe import LazyFrame
     from narwhals.dtypes import DType
     from narwhals.typing import AsofJoinStrategy, JoinStrategy, LazyUniqueKeepStrategy
@@ -46,32 +44,32 @@ Very low priority until `dask` adds typing.
 
 
 class DaskLazyFrame(
-    CompliantLazyFrame["DaskExpr", "dd.DataFrame", "LazyFrame[dd.DataFrame]"]
+    CompliantLazyFrame["DaskExpr", "dd.DataFrame", "LazyFrame[dd.DataFrame]"],
+    ValidateBackendVersion,
 ):
+    _implementation = Implementation.DASK
+
     def __init__(
         self,
         native_dataframe: dd.DataFrame,
         *,
-        backend_version: tuple[int, ...],
         version: Version,
+        validate_backend_version: bool = False,
     ) -> None:
         self._native_frame: dd.DataFrame = native_dataframe
-        self._backend_version = backend_version
-        self._implementation = Implementation.DASK
         self._version = version
         self._cached_schema: dict[str, DType] | None = None
         self._cached_columns: list[str] | None = None
-        validate_backend_version(self._implementation, self._backend_version)
+        if validate_backend_version:
+            self._validate_backend_version()
 
     @staticmethod
     def _is_native(obj: dd.DataFrame | Any) -> TypeIs[dd.DataFrame]:
         return isinstance(obj, dd.DataFrame)
 
     @classmethod
-    def from_native(cls, data: dd.DataFrame, /, *, context: _FullContext) -> Self:
-        return cls(
-            data, backend_version=context._backend_version, version=context._version
-        )
+    def from_native(cls, data: dd.DataFrame, /, *, context: _LimitedContext) -> Self:
+        return cls(data, version=context._version)
 
     def to_narwhals(self) -> LazyFrame[dd.DataFrame]:
         return self._version.lazyframe(self, level="lazy")
@@ -86,20 +84,16 @@ class DaskLazyFrame(
     def __narwhals_namespace__(self) -> DaskNamespace:
         from narwhals._dask.namespace import DaskNamespace
 
-        return DaskNamespace(backend_version=self._backend_version, version=self._version)
+        return DaskNamespace(version=self._version)
 
     def __narwhals_lazyframe__(self) -> Self:
         return self
 
     def _with_version(self, version: Version) -> Self:
-        return self.__class__(
-            self.native, backend_version=self._backend_version, version=version
-        )
+        return self.__class__(self.native, version=version)
 
     def _with_native(self, df: Any) -> Self:
-        return self.__class__(
-            df, backend_version=self._backend_version, version=self._version
-        )
+        return self.__class__(df, version=self._version)
 
     def _iter_columns(self) -> Iterator[dx.Series]:
         for _col, ser in self.native.items():  # noqa: PERF102
@@ -120,7 +114,7 @@ class DaskLazyFrame(
             return PandasLikeDataFrame(
                 result,
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=True,
             )
@@ -132,7 +126,7 @@ class DaskLazyFrame(
 
             return PolarsDataFrame(
                 pl.from_pandas(result),
-                backend_version=parse_version(pl),
+                validate_backend_version=True,
                 version=self._version,
             )
 
@@ -143,7 +137,7 @@ class DaskLazyFrame(
 
             return ArrowDataFrame(
                 pa.Table.from_pandas(result),
-                backend_version=parse_version(pa),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=True,
             )
@@ -168,9 +162,7 @@ class DaskLazyFrame(
 
     def simple_select(self, *column_names: str) -> Self:
         df: Incomplete = self.native
-        native = select_columns_by_name(
-            df, list(column_names), self._backend_version, self._implementation
-        )
+        native = select_columns_by_name(df, list(column_names), self._implementation)
         return self._with_native(native)
 
     def aggregate(self, *exprs: DaskExpr) -> Self:
@@ -184,7 +176,6 @@ class DaskLazyFrame(
         df = select_columns_by_name(
             df.assign(**dict(new_series)),
             [s[0] for s in new_series],
-            self._backend_version,
             self._implementation,
         )
         return self._with_native(df)
@@ -220,11 +211,7 @@ class DaskLazyFrame(
         # Implementation is based on the following StackOverflow reply:
         # https://stackoverflow.com/questions/60831518/in-dask-how-does-one-add-a-range-of-integersauto-increment-to-a-new-column/60852409#60852409
         if order_by is None:
-            return self._with_native(
-                add_row_index(
-                    self.native, name, self._backend_version, self._implementation
-                )
-            )
+            return self._with_native(add_row_index(self.native, name))
         else:
             plx = self.__narwhals_namespace__()
             columns = self.columns
@@ -376,14 +363,9 @@ class DaskLazyFrame(
         Notice that a native object is returned.
         """
         other_native: Incomplete = other.native
+        # rename to avoid creating extra columns in join
         return (
-            select_columns_by_name(
-                other_native,
-                column_names=columns_to_select,
-                backend_version=self._backend_version,
-                implementation=self._implementation,
-            )
-            # rename to avoid creating extra columns in join
+            select_columns_by_name(other_native, columns_to_select, self._implementation)
             .rename(columns=columns_mapping)
             .drop_duplicates()
         )
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index a77dce3d..e9f4ec4e 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
     from narwhals._dask.dataframe import DaskLazyFrame
     from narwhals._dask.namespace import DaskNamespace
     from narwhals._expression_parsing import ExprKind, ExprMetadata
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.typing import (
         FillNullStrategy,
         IntoDType,
@@ -55,7 +55,6 @@ class DaskExpr(
         function_name: str,
         evaluate_output_names: EvalNames[DaskLazyFrame],
         alias_output_names: AliasNames | None,
-        backend_version: tuple[int, ...],
         version: Version,
         scalar_kwargs: ScalarKwargs | None = None,
     ) -> None:
@@ -64,7 +63,6 @@ class DaskExpr(
         self._function_name = function_name
         self._evaluate_output_names = evaluate_output_names
         self._alias_output_names = alias_output_names
-        self._backend_version = backend_version
         self._version = version
         self._scalar_kwargs = scalar_kwargs or {}
         self._metadata: ExprMetadata | None = None
@@ -75,10 +73,9 @@ class DaskExpr(
     def __narwhals_expr__(self) -> None: ...
 
     def __narwhals_namespace__(self) -> DaskNamespace:  # pragma: no cover
-        # Unused, just for compatibility with PandasLikeExpr
         from narwhals._dask.namespace import DaskNamespace
 
-        return DaskNamespace(backend_version=self._backend_version, version=self._version)
+        return DaskNamespace(version=self._version)
 
     def broadcast(self, kind: Literal[ExprKind.AGGREGATION, ExprKind.LITERAL]) -> Self:
         def func(df: DaskLazyFrame) -> list[dx.Series]:
@@ -92,7 +89,6 @@ class DaskExpr(
             function_name=self._function_name,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
             scalar_kwargs=self._scalar_kwargs,
         )
@@ -103,7 +99,7 @@ class DaskExpr(
         evaluate_column_names: EvalNames[DaskLazyFrame],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         function_name: str = "",
     ) -> Self:
         def func(df: DaskLazyFrame) -> list[dx.Series]:
@@ -123,12 +119,11 @@ class DaskExpr(
             function_name=function_name,
             evaluate_output_names=evaluate_column_names,
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
     @classmethod
-    def from_column_indices(cls, *column_indices: int, context: _FullContext) -> Self:
+    def from_column_indices(cls, *column_indices: int, context: _LimitedContext) -> Self:
         def func(df: DaskLazyFrame) -> list[dx.Series]:
             return [df.native.iloc[:, i] for i in column_indices]
 
@@ -138,7 +133,6 @@ class DaskExpr(
             function_name="nth",
             evaluate_output_names=cls._eval_names_indices(column_indices),
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
@@ -169,7 +163,6 @@ class DaskExpr(
             function_name=f"{self._function_name}->{expr_name}",
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
             scalar_kwargs=scalar_kwargs,
         )
@@ -181,7 +174,6 @@ class DaskExpr(
             function_name=self._function_name,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=func,
-            backend_version=self._backend_version,
             version=self._version,
             scalar_kwargs=self._scalar_kwargs,
         )
@@ -528,9 +520,7 @@ class DaskExpr(
         def func(expr: dx.Series) -> dx.Series:
             _name = expr.name
             col_token = generate_temporary_column_name(n_bytes=8, columns=[_name])
-            frame = add_row_index(
-                expr.to_frame(), col_token, self._backend_version, self._implementation
-            )
+            frame = add_row_index(expr.to_frame(), col_token)
             first_distinct_index = frame.groupby(_name).agg({col_token: "min"})[col_token]
             return frame[col_token].isin(first_distinct_index)
 
@@ -540,9 +530,7 @@ class DaskExpr(
         def func(expr: dx.Series) -> dx.Series:
             _name = expr.name
             col_token = generate_temporary_column_name(n_bytes=8, columns=[_name])
-            frame = add_row_index(
-                expr.to_frame(), col_token, self._backend_version, self._implementation
-            )
+            frame = add_row_index(expr.to_frame(), col_token)
             last_distinct_index = frame.groupby(_name).agg({col_token: "max"})[col_token]
             return frame[col_token].isin(last_distinct_index)
 
@@ -635,7 +623,6 @@ class DaskExpr(
             function_name=self._function_name + "->over",
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
diff --git a/narwhals/_dask/group_by.py b/narwhals/_dask/group_by.py
index 2e4e894e..1a227ed0 100644
--- a/narwhals/_dask/group_by.py
+++ b/narwhals/_dask/group_by.py
@@ -120,6 +120,5 @@ class DaskLazyGroupBy(DepthTrackingGroupBy["DaskLazyFrame", "DaskExpr", Aggregat
             )
         return DaskLazyFrame(
             self._grouped.agg(**simple_aggregations).reset_index(),
-            backend_version=self.compliant._backend_version,
             version=self.compliant._version,
         ).rename(dict(zip(self._keys, self._output_key_names)))
diff --git a/narwhals/_dask/namespace.py b/narwhals/_dask/namespace.py
index 3177e4a1..e1a69042 100644
--- a/narwhals/_dask/namespace.py
+++ b/narwhals/_dask/namespace.py
@@ -55,8 +55,7 @@ class DaskNamespace(
     def _lazyframe(self) -> type[DaskLazyFrame]:
         return DaskLazyFrame
 
-    def __init__(self, *, backend_version: tuple[int, ...], version: Version) -> None:
-        self._backend_version = backend_version
+    def __init__(self, *, version: Version) -> None:
         self._version = version
 
     def lit(self, value: NonNestedLiteral, dtype: IntoDType | None) -> DaskExpr:
@@ -76,7 +75,6 @@ class DaskNamespace(
             function_name="lit",
             evaluate_output_names=lambda _df: ["literal"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -91,7 +89,6 @@ class DaskNamespace(
             function_name="len",
             evaluate_output_names=lambda _df: ["len"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -117,7 +114,6 @@ class DaskNamespace(
             function_name="all_horizontal",
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -143,7 +139,6 @@ class DaskNamespace(
             function_name="any_horizontal",
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -160,7 +155,6 @@ class DaskNamespace(
             function_name="sum_horizontal",
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -185,15 +179,11 @@ class DaskNamespace(
                     )
                     raise TypeError(msg)
             return DaskLazyFrame(
-                dd.concat(dfs, axis=0, join="inner"),
-                backend_version=self._backend_version,
-                version=self._version,
+                dd.concat(dfs, axis=0, join="inner"), version=self._version
             )
         if how == "diagonal":
             return DaskLazyFrame(
-                dd.concat(dfs, axis=0, join="outer"),
-                backend_version=self._backend_version,
-                version=self._version,
+                dd.concat(dfs, axis=0, join="outer"), version=self._version
             )
 
         raise NotImplementedError
@@ -215,7 +205,6 @@ class DaskNamespace(
             function_name="mean_horizontal",
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -233,7 +222,6 @@ class DaskNamespace(
             function_name="min_horizontal",
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -251,7 +239,6 @@ class DaskNamespace(
             function_name="max_horizontal",
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -296,7 +283,6 @@ class DaskNamespace(
                 exprs[0], "_evaluate_output_names", lambda _df: ["literal"]
             ),
             alias_output_names=getattr(exprs[0], "_alias_output_names", None),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
diff --git a/narwhals/_dask/selectors.py b/narwhals/_dask/selectors.py
index 218b1e35..41120016 100644
--- a/narwhals/_dask/selectors.py
+++ b/narwhals/_dask/selectors.py
@@ -25,6 +25,5 @@ class DaskSelector(CompliantSelector["DaskLazyFrame", "dx.Series"], DaskExpr):
             function_name=self._function_name,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
diff --git a/narwhals/_dask/utils.py b/narwhals/_dask/utils.py
index 97de9aed..2c396b36 100644
--- a/narwhals/_dask/utils.py
+++ b/narwhals/_dask/utils.py
@@ -3,13 +3,8 @@ from __future__ import annotations
 from typing import TYPE_CHECKING, Any
 
 from narwhals._pandas_like.utils import select_columns_by_name
-from narwhals._utils import (
-    Implementation,
-    Version,
-    isinstance_or_issubclass,
-    parse_version,
-)
-from narwhals.dependencies import get_pandas, get_pyarrow
+from narwhals._utils import Implementation, Version, isinstance_or_issubclass
+from narwhals.dependencies import get_pyarrow
 
 if TYPE_CHECKING:
     from collections.abc import Sequence
@@ -58,19 +53,13 @@ def align_series_full_broadcast(
     ]  # pyright: ignore[reportReturnType]
 
 
-def add_row_index(
-    frame: dd.DataFrame,
-    name: str,
-    backend_version: tuple[int, ...],
-    implementation: Implementation,
-) -> dd.DataFrame:
+def add_row_index(frame: dd.DataFrame, name: str) -> dd.DataFrame:
     original_cols = frame.columns
     df: Incomplete = frame.assign(**{name: 1})
     return select_columns_by_name(
         df.assign(**{name: df[name].cumsum(method="blelloch") - 1}),
         [name, *original_cols],
-        backend_version,
-        implementation,
+        Implementation.DASK,
     )
 
 
@@ -113,7 +102,7 @@ def narwhals_to_native_dtype(dtype: IntoDType, version: Version) -> Any:  # noqa
     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) >= (2, 0, 0):
+        if Implementation.PANDAS._backend_version() >= (2, 0, 0):
             if get_pyarrow() is not None:
                 return "string[pyarrow]"
             return "string[python]"  # pragma: no cover
@@ -130,7 +119,7 @@ def narwhals_to_native_dtype(dtype: IntoDType, version: Version) -> Any:  # noqa
             # NOTE: `pandas-stubs.core.dtypes.dtypes.CategoricalDtype.categories` is too narrow
             # Should be one of the `ListLike*` types
             # https://github.com/pandas-dev/pandas-stubs/blob/8434bde95460b996323cc8c0fea7b0a8bb00ea26/pandas-stubs/_typing.pyi#L497-L505
-            return pd.CategoricalDtype(dtype.categories, ordered=True)  # pyright: ignore[reportArgumentType]
+            return pd.CategoricalDtype(dtype.categories, ordered=True)  # type: ignore[arg-type]
         msg = "Can not cast / initialize Enum without categories present"
         raise ValueError(msg)
 
diff --git a/narwhals/_duckdb/dataframe.py b/narwhals/_duckdb/dataframe.py
index 63e0e97d..eca7978a 100644
--- a/narwhals/_duckdb/dataframe.py
+++ b/narwhals/_duckdb/dataframe.py
@@ -18,13 +18,12 @@ from narwhals._duckdb.utils import (
 )
 from narwhals._utils import (
     Implementation,
+    ValidateBackendVersion,
     Version,
     generate_temporary_column_name,
     not_implemented,
     parse_columns_to_drop,
-    parse_version,
     requires,
-    validate_backend_version,
 )
 from narwhals.dependencies import get_duckdb
 from narwhals.exceptions import InvalidOperationError
@@ -45,7 +44,7 @@ if TYPE_CHECKING:
     from narwhals._duckdb.group_by import DuckDBGroupBy
     from narwhals._duckdb.namespace import DuckDBNamespace
     from narwhals._duckdb.series import DuckDBInterchangeSeries
-    from narwhals._utils import _FullContext
+    from narwhals._utils import _LimitedContext
     from narwhals.dataframe import LazyFrame
     from narwhals.dtypes import DType
     from narwhals.stable.v1 import DataFrame as DataFrameV1
@@ -57,7 +56,8 @@ class DuckDBLazyFrame(
         "DuckDBExpr",
         "duckdb.DuckDBPyRelation",
         "LazyFrame[duckdb.DuckDBPyRelation] | DataFrameV1[duckdb.DuckDBPyRelation]",
-    ]
+    ],
+    ValidateBackendVersion,
 ):
     _implementation = Implementation.DUCKDB
 
@@ -65,15 +65,19 @@ class DuckDBLazyFrame(
         self,
         df: duckdb.DuckDBPyRelation,
         *,
-        backend_version: tuple[int, ...],
         version: Version,
+        validate_backend_version: bool = False,
     ) -> None:
         self._native_frame: duckdb.DuckDBPyRelation = df
         self._version = version
-        self._backend_version = backend_version
         self._cached_native_schema: dict[str, DuckDBPyType] | None = None
         self._cached_columns: list[str] | None = None
-        validate_backend_version(self._implementation, self._backend_version)
+        if validate_backend_version:
+            self._validate_backend_version()
+
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
 
     @staticmethod
     def _is_native(obj: duckdb.DuckDBPyRelation | Any) -> TypeIs[duckdb.DuckDBPyRelation]:
@@ -81,11 +85,9 @@ class DuckDBLazyFrame(
 
     @classmethod
     def from_native(
-        cls, data: duckdb.DuckDBPyRelation, /, *, context: _FullContext
+        cls, data: duckdb.DuckDBPyRelation, /, *, context: _LimitedContext
     ) -> Self:
-        return cls(
-            data, backend_version=context._backend_version, version=context._version
-        )
+        return cls(data, version=context._version)
 
     def to_narwhals(
         self, *args: Any, **kwds: Any
@@ -113,9 +115,7 @@ class DuckDBLazyFrame(
     def __narwhals_namespace__(self) -> DuckDBNamespace:
         from narwhals._duckdb.namespace import DuckDBNamespace
 
-        return DuckDBNamespace(
-            backend_version=self._backend_version, version=self._version
-        )
+        return DuckDBNamespace(version=self._version)
 
     def get_column(self, name: str) -> DuckDBInterchangeSeries:
         from narwhals._duckdb.series import DuckDBInterchangeSeries
@@ -130,37 +130,31 @@ class DuckDBLazyFrame(
         self, backend: ModuleType | Implementation | str | None, **kwargs: Any
     ) -> CompliantDataFrameAny:
         if backend is None or backend is Implementation.PYARROW:
-            import pyarrow as pa  # ignore-banned-import
-
             from narwhals._arrow.dataframe import ArrowDataFrame
 
             return ArrowDataFrame(
                 self.native.arrow(),
-                backend_version=parse_version(pa),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=True,
             )
 
         if backend is Implementation.PANDAS:
-            import pandas as pd  # ignore-banned-import
-
             from narwhals._pandas_like.dataframe import PandasLikeDataFrame
 
             return PandasLikeDataFrame(
                 self.native.df(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=True,
             )
 
         if backend is Implementation.POLARS:
-            import polars as pl  # ignore-banned-import
-
             from narwhals._polars.dataframe import PolarsDataFrame
 
             return PolarsDataFrame(
-                self.native.pl(), backend_version=parse_version(pl), version=self._version
+                self.native.pl(), validate_backend_version=True, version=self._version
             )
 
         msg = f"Unsupported `backend` value: {backend}"  # pragma: no cover
@@ -238,27 +232,17 @@ class DuckDBLazyFrame(
 
     def to_pandas(self) -> pd.DataFrame:
         # only if version is v1, keep around for backcompat
-        import pandas as pd  # ignore-banned-import()
-
-        if parse_version(pd) >= (1, 0, 0):
-            return self.native.df()
-        else:  # pragma: no cover
-            msg = f"Conversion to pandas requires 'pandas>=1.0.0', found {pd.__version__}"
-            raise NotImplementedError(msg)
+        return self.native.df()
 
     def to_arrow(self) -> pa.Table:
         # only if version is v1, keep around for backcompat
         return self.native.arrow()
 
     def _with_version(self, version: Version) -> Self:
-        return self.__class__(
-            self.native, version=version, backend_version=self._backend_version
-        )
+        return self.__class__(self.native, version=version)
 
     def _with_native(self, df: duckdb.DuckDBPyRelation) -> Self:
-        return self.__class__(
-            df, backend_version=self._backend_version, version=self._version
-        )
+        return self.__class__(df, version=self._version)
 
     def group_by(
         self, keys: Sequence[str] | Sequence[DuckDBExpr], *, drop_null_keys: bool
diff --git a/narwhals/_duckdb/expr.py b/narwhals/_duckdb/expr.py
index 29bd41a3..8762c9d8 100644
--- a/narwhals/_duckdb/expr.py
+++ b/narwhals/_duckdb/expr.py
@@ -42,7 +42,7 @@ if TYPE_CHECKING:
     from narwhals._duckdb.namespace import DuckDBNamespace
     from narwhals._duckdb.typing import WindowExpressionKwargs
     from narwhals._expression_parsing import ExprMetadata
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.typing import (
         FillNullStrategy,
         IntoDType,
@@ -67,13 +67,11 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
         *,
         evaluate_output_names: EvalNames[DuckDBLazyFrame],
         alias_output_names: AliasNames | None,
-        backend_version: tuple[int, ...],
         version: Version,
     ) -> None:
         self._call = call
         self._evaluate_output_names = evaluate_output_names
         self._alias_output_names = alias_output_names
-        self._backend_version = backend_version
         self._version = version
         self._metadata: ExprMetadata | None = None
         self._window_function: DuckDBWindowFunction | None = window_function
@@ -97,12 +95,9 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
     def __narwhals_expr__(self) -> None: ...
 
     def __narwhals_namespace__(self) -> DuckDBNamespace:  # pragma: no cover
-        # Unused, just for compatibility with PandasLikeExpr
         from narwhals._duckdb.namespace import DuckDBNamespace
 
-        return DuckDBNamespace(
-            backend_version=self._backend_version, version=self._version
-        )
+        return DuckDBNamespace(version=self._version)
 
     def _cum_window_func(
         self,
@@ -193,7 +188,7 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
         evaluate_column_names: EvalNames[DuckDBLazyFrame],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
     ) -> Self:
         def func(df: DuckDBLazyFrame) -> list[Expression]:
             return [col(name) for name in evaluate_column_names(df)]
@@ -202,12 +197,11 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             func,
             evaluate_output_names=evaluate_column_names,
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
     @classmethod
-    def from_column_indices(cls, *column_indices: int, context: _FullContext) -> Self:
+    def from_column_indices(cls, *column_indices: int, context: _LimitedContext) -> Self:
         def func(df: DuckDBLazyFrame) -> list[Expression]:
             columns = df.columns
             return [col(columns[i]) for i in column_indices]
@@ -216,7 +210,6 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             func,
             evaluate_output_names=cls._eval_names_indices(column_indices),
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
@@ -242,7 +235,6 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             window_function=window_function,
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=context._backend_version,
             version=context._version,
         )
 
@@ -300,7 +292,6 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             self._callable_to_eval_series(call, **expressifiable_args),
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -312,7 +303,6 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             self._push_down_window_function(call, **expressifiable_args),
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -322,7 +312,6 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             self._push_down_window_function(op, other=other),
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -332,7 +321,6 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             self._window_function,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=func,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -342,7 +330,6 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             window_function,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -525,7 +512,6 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
             func,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
diff --git a/narwhals/_duckdb/expr_dt.py b/narwhals/_duckdb/expr_dt.py
index f4415595..92b48042 100644
--- a/narwhals/_duckdb/expr_dt.py
+++ b/narwhals/_duckdb/expr_dt.py
@@ -131,7 +131,6 @@ class DuckDBExprDateTimeNamespace(
             func,
             evaluate_output_names=self.compliant._evaluate_output_names,
             alias_output_names=self.compliant._alias_output_names,
-            backend_version=self.compliant._backend_version,
             version=self.compliant._version,
         )
 
diff --git a/narwhals/_duckdb/namespace.py b/narwhals/_duckdb/namespace.py
index bfc84632..a4cea2b5 100644
--- a/narwhals/_duckdb/namespace.py
+++ b/narwhals/_duckdb/namespace.py
@@ -33,8 +33,7 @@ class DuckDBNamespace(
 ):
     _implementation: Implementation = Implementation.DUCKDB
 
-    def __init__(self, *, backend_version: tuple[int, ...], version: Version) -> None:
-        self._backend_version = backend_version
+    def __init__(self, *, version: Version) -> None:
         self._version = version
 
     @property
@@ -87,7 +86,6 @@ class DuckDBNamespace(
             call=func,
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -157,7 +155,6 @@ class DuckDBNamespace(
             func,
             evaluate_output_names=lambda _df: ["literal"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -169,7 +166,6 @@ class DuckDBNamespace(
             call=func,
             evaluate_output_names=lambda _df: ["len"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
diff --git a/narwhals/_duckdb/selectors.py b/narwhals/_duckdb/selectors.py
index ea1c6bac..ea8db0ea 100644
--- a/narwhals/_duckdb/selectors.py
+++ b/narwhals/_duckdb/selectors.py
@@ -26,6 +26,5 @@ class DuckDBSelector(  # type: ignore[misc]
             self._window_function,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
diff --git a/narwhals/_ibis/dataframe.py b/narwhals/_ibis/dataframe.py
index 0c96b08a..267a7812 100644
--- a/narwhals/_ibis/dataframe.py
+++ b/narwhals/_ibis/dataframe.py
@@ -9,11 +9,10 @@ import ibis.expr.types as ir
 from narwhals._ibis.utils import evaluate_exprs, native_to_narwhals_dtype
 from narwhals._utils import (
     Implementation,
+    ValidateBackendVersion,
     Version,
     not_implemented,
     parse_columns_to_drop,
-    parse_version,
-    validate_backend_version,
 )
 from narwhals.exceptions import ColumnNotFoundError, InvalidOperationError
 from narwhals.typing import CompliantLazyFrame
@@ -32,7 +31,7 @@ if TYPE_CHECKING:
     from narwhals._ibis.group_by import IbisGroupBy
     from narwhals._ibis.namespace import IbisNamespace
     from narwhals._ibis.series import IbisInterchangeSeries
-    from narwhals._utils import _FullContext
+    from narwhals._utils import _LimitedContext
     from narwhals.dataframe import LazyFrame
     from narwhals.dtypes import DType
     from narwhals.stable.v1 import DataFrame as DataFrameV1
@@ -44,29 +43,28 @@ if TYPE_CHECKING:
 class IbisLazyFrame(
     CompliantLazyFrame[
         "IbisExpr", "ir.Table", "LazyFrame[ir.Table] | DataFrameV1[ir.Table]"
-    ]
+    ],
+    ValidateBackendVersion,
 ):
     _implementation = Implementation.IBIS
 
     def __init__(
-        self, df: ir.Table, *, backend_version: tuple[int, ...], version: Version
+        self, df: ir.Table, *, version: Version, validate_backend_version: bool = False
     ) -> None:
         self._native_frame: ir.Table = df
         self._version = version
-        self._backend_version = backend_version
         self._cached_schema: dict[str, DType] | None = None
         self._cached_columns: list[str] | None = None
-        validate_backend_version(self._implementation, self._backend_version)
+        if validate_backend_version:
+            self._validate_backend_version()
 
     @staticmethod
     def _is_native(obj: ir.Table | Any) -> TypeIs[ir.Table]:
         return isinstance(obj, ir.Table)
 
     @classmethod
-    def from_native(cls, data: ir.Table, /, *, context: _FullContext) -> Self:
-        return cls(
-            data, backend_version=context._backend_version, version=context._version
-        )
+    def from_native(cls, data: ir.Table, /, *, context: _LimitedContext) -> Self:
+        return cls(data, version=context._version)
 
     def to_narwhals(self) -> LazyFrame[ir.Table] | DataFrameV1[ir.Table]:
         if self._version is Version.MAIN:
@@ -92,7 +90,7 @@ class IbisLazyFrame(
     def __narwhals_namespace__(self) -> IbisNamespace:
         from narwhals._ibis.namespace import IbisNamespace
 
-        return IbisNamespace(backend_version=self._backend_version, version=self._version)
+        return IbisNamespace(version=self._version)
 
     def get_column(self, name: str) -> IbisInterchangeSeries:
         from narwhals._ibis.series import IbisInterchangeSeries
@@ -107,38 +105,32 @@ class IbisLazyFrame(
         self, backend: ModuleType | Implementation | str | None, **kwargs: Any
     ) -> CompliantDataFrameAny:
         if backend is None or backend is Implementation.PYARROW:
-            import pyarrow as pa  # ignore-banned-import
-
             from narwhals._arrow.dataframe import ArrowDataFrame
 
             return ArrowDataFrame(
                 self.native.to_pyarrow(),
-                backend_version=parse_version(pa),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=True,
             )
 
         if backend is Implementation.PANDAS:
-            import pandas as pd  # ignore-banned-import
-
             from narwhals._pandas_like.dataframe import PandasLikeDataFrame
 
             return PandasLikeDataFrame(
                 self.native.to_pandas(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=True,
             )
 
         if backend is Implementation.POLARS:
-            import polars as pl  # ignore-banned-import
-
             from narwhals._polars.dataframe import PolarsDataFrame
 
             return PolarsDataFrame(
                 self.native.to_polars(),
-                backend_version=parse_version(pl),
+                validate_backend_version=True,
                 version=self._version,
             )
 
@@ -214,27 +206,17 @@ class IbisLazyFrame(
 
     def to_pandas(self) -> pd.DataFrame:
         # only if version is v1, keep around for backcompat
-        import pandas as pd  # ignore-banned-import()
-
-        if parse_version(pd) >= (1, 0, 0):
-            return self.native.to_pandas()
-        else:  # pragma: no cover
-            msg = f"Conversion to pandas requires pandas>=1.0.0, found {pd.__version__}"
-            raise NotImplementedError(msg)
+        return self.native.to_pandas()
 
     def to_arrow(self) -> pa.Table:
         # only if version is v1, keep around for backcompat
         return self.native.to_pyarrow()
 
     def _with_version(self, version: Version) -> Self:
-        return self.__class__(
-            self.native, version=version, backend_version=self._backend_version
-        )
+        return self.__class__(self.native, version=version)
 
     def _with_native(self, df: ir.Table) -> Self:
-        return self.__class__(
-            df, backend_version=self._backend_version, version=self._version
-        )
+        return self.__class__(df, version=self._version)
 
     def group_by(
         self, keys: Sequence[str] | Sequence[IbisExpr], *, drop_null_keys: bool
diff --git a/narwhals/_ibis/expr.py b/narwhals/_ibis/expr.py
index 704c1bc1..7f4dab27 100644
--- a/narwhals/_ibis/expr.py
+++ b/narwhals/_ibis/expr.py
@@ -33,7 +33,7 @@ if TYPE_CHECKING:
     from narwhals._expression_parsing import ExprKind, ExprMetadata
     from narwhals._ibis.dataframe import IbisLazyFrame
     from narwhals._ibis.namespace import IbisNamespace
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.typing import IntoDType, RankMethod, RollingInterpolationMethod
 
     ExprT = TypeVar("ExprT", bound=ir.Value)
@@ -51,13 +51,11 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
         *,
         evaluate_output_names: EvalNames[IbisLazyFrame],
         alias_output_names: AliasNames | None,
-        backend_version: tuple[int, ...],
         version: Version,
     ) -> None:
         self._call = call
         self._evaluate_output_names = evaluate_output_names
         self._alias_output_names = alias_output_names
-        self._backend_version = backend_version
         self._version = version
         self._metadata: ExprMetadata | None = None
         self._window_function: IbisWindowFunction | None = window_function
@@ -81,10 +79,9 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
     def __narwhals_expr__(self) -> None: ...
 
     def __narwhals_namespace__(self) -> IbisNamespace:  # pragma: no cover
-        # Unused, just for compatibility with PandasLikeExpr
         from narwhals._ibis.namespace import IbisNamespace
 
-        return IbisNamespace(backend_version=self._backend_version, version=self._version)
+        return IbisNamespace(version=self._version)
 
     def _cum_window_func(
         self, *, reverse: bool, func_name: Literal["sum", "max", "min", "count"]
@@ -180,7 +177,7 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
         evaluate_column_names: EvalNames[IbisLazyFrame],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
     ) -> Self:
         def func(df: IbisLazyFrame) -> list[ir.Column]:
             return [df.native[name] for name in evaluate_column_names(df)]
@@ -189,12 +186,11 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
             func,
             evaluate_output_names=evaluate_column_names,
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
     @classmethod
-    def from_column_indices(cls, *column_indices: int, context: _FullContext) -> Self:
+    def from_column_indices(cls, *column_indices: int, context: _LimitedContext) -> Self:
         def func(df: IbisLazyFrame) -> list[ir.Column]:
             return [df.native[i] for i in column_indices]
 
@@ -202,7 +198,6 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
             func,
             evaluate_output_names=cls._eval_names_indices(column_indices),
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
@@ -219,7 +214,6 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
             call=call,
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=context._backend_version,
             version=context._version,
         )
 
@@ -250,7 +244,6 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
             func,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -263,7 +256,6 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
             self._window_function,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=func,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -273,7 +265,6 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
             window_function,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -339,7 +330,6 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
             func,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -388,7 +378,6 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
             func,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
diff --git a/narwhals/_ibis/namespace.py b/narwhals/_ibis/namespace.py
index 5c8dd40e..62eec464 100644
--- a/narwhals/_ibis/namespace.py
+++ b/narwhals/_ibis/namespace.py
@@ -29,8 +29,7 @@ if TYPE_CHECKING:
 class IbisNamespace(LazyNamespace[IbisLazyFrame, IbisExpr, "ir.Table"]):
     _implementation: Implementation = Implementation.IBIS
 
-    def __init__(self, *, backend_version: tuple[int, ...], version: Version) -> None:
-        self._backend_version = backend_version
+    def __init__(self, *, version: Version) -> None:
         self._version = version
 
     @property
@@ -81,7 +80,6 @@ class IbisNamespace(LazyNamespace[IbisLazyFrame, IbisExpr, "ir.Table"]):
             call=func,
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -148,7 +146,6 @@ class IbisNamespace(LazyNamespace[IbisLazyFrame, IbisExpr, "ir.Table"]):
             func,
             evaluate_output_names=lambda _df: ["literal"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -160,7 +157,6 @@ class IbisNamespace(LazyNamespace[IbisLazyFrame, IbisExpr, "ir.Table"]):
             call=func,
             evaluate_output_names=lambda _df: ["len"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
diff --git a/narwhals/_ibis/selectors.py b/narwhals/_ibis/selectors.py
index f96243b0..3fa8d6cd 100644
--- a/narwhals/_ibis/selectors.py
+++ b/narwhals/_ibis/selectors.py
@@ -25,6 +25,5 @@ class IbisSelector(  # type: ignore[misc]
             self._call,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
         )
diff --git a/narwhals/_namespace.py b/narwhals/_namespace.py
index d894e8a9..56670a0c 100644
--- a/narwhals/_namespace.py
+++ b/narwhals/_namespace.py
@@ -256,42 +256,38 @@ class Namespace(Generic[CompliantNamespaceT_co]):
             Namespace[PolarsNamespace]
         """
         impl = Implementation.from_backend(backend)
-        backend_version = impl._backend_version()
+        backend_version = impl._backend_version()  # noqa: F841
         version = cls._version
         ns: CompliantNamespaceAny
         if impl.is_pandas_like():
             from narwhals._pandas_like.namespace import PandasLikeNamespace
 
-            ns = PandasLikeNamespace(
-                implementation=impl, backend_version=backend_version, version=version
-            )
+            ns = PandasLikeNamespace(implementation=impl, version=version)
 
         elif impl.is_polars():
             from narwhals._polars.namespace import PolarsNamespace
 
-            ns = PolarsNamespace(backend_version=backend_version, version=version)
+            ns = PolarsNamespace(version=version)
         elif impl.is_pyarrow():
             from narwhals._arrow.namespace import ArrowNamespace
 
-            ns = ArrowNamespace(backend_version=backend_version, version=version)
+            ns = ArrowNamespace(version=version)
         elif impl.is_spark_like():
             from narwhals._spark_like.namespace import SparkLikeNamespace
 
-            ns = SparkLikeNamespace(
-                implementation=impl, backend_version=backend_version, version=version
-            )
+            ns = SparkLikeNamespace(implementation=impl, version=version)
         elif impl.is_duckdb():
             from narwhals._duckdb.namespace import DuckDBNamespace
 
-            ns = DuckDBNamespace(backend_version=backend_version, version=version)
+            ns = DuckDBNamespace(version=version)
         elif impl.is_dask():
             from narwhals._dask.namespace import DaskNamespace
 
-            ns = DaskNamespace(backend_version=backend_version, version=version)
+            ns = DaskNamespace(version=version)
         elif impl.is_ibis():
             from narwhals._ibis.namespace import IbisNamespace
 
-            ns = IbisNamespace(backend_version=backend_version, version=version)
+            ns = IbisNamespace(version=version)
         else:
             msg = "Not supported Implementation"  # pragma: no cover
             raise AssertionError(msg)
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index a4d606de..9cad81a3 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -27,9 +27,7 @@ from narwhals._utils import (
     exclude_column_names,
     generate_temporary_column_name,
     parse_columns_to_drop,
-    parse_version,
     scale_bytes,
-    validate_backend_version,
 )
 from narwhals.dependencies import is_pandas_like_dataframe
 from narwhals.exceptions import InvalidOperationError, ShapeError
@@ -48,7 +46,7 @@ if TYPE_CHECKING:
     from narwhals._pandas_like.group_by import PandasLikeGroupBy
     from narwhals._pandas_like.namespace import PandasLikeNamespace
     from narwhals._translate import IntoArrowTable
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.dtypes import DType
     from narwhals.schema import Schema
     from narwhals.typing import (
@@ -102,20 +100,20 @@ class PandasLikeDataFrame(
         native_dataframe: Any,
         *,
         implementation: Implementation,
-        backend_version: tuple[int, ...],
         version: Version,
         validate_column_names: bool,
+        validate_backend_version: bool = False,
     ) -> None:
         self._native_frame = native_dataframe
         self._implementation = implementation
-        self._backend_version = backend_version
         self._version = version
-        validate_backend_version(self._implementation, self._backend_version)
         if validate_column_names:
             check_column_names_are_unique(native_dataframe.columns)
+        if validate_backend_version:
+            self._validate_backend_version()
 
     @classmethod
-    def from_arrow(cls, data: IntoArrowTable, /, *, context: _FullContext) -> Self:
+    def from_arrow(cls, data: IntoArrowTable, /, *, context: _LimitedContext) -> Self:
         implementation = context._implementation
         tbl = _into_arrow_table(data, context)
         if implementation.is_pandas():
@@ -139,7 +137,7 @@ class PandasLikeDataFrame(
         data: Mapping[str, Any],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         schema: Mapping[str, DType] | Schema | None,
     ) -> Self:
         from narwhals.schema import Schema
@@ -174,11 +172,10 @@ class PandasLikeDataFrame(
         return is_pandas_like_dataframe(obj)  # pragma: no cover
 
     @classmethod
-    def from_native(cls, data: Any, /, *, context: _FullContext) -> Self:
+    def from_native(cls, data: Any, /, *, context: _LimitedContext) -> Self:
         return cls(
             data,
             implementation=context._implementation,
-            backend_version=context._backend_version,
             version=context._version,
             validate_column_names=True,
         )
@@ -189,7 +186,7 @@ class PandasLikeDataFrame(
         data: _2DArray,
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         schema: Mapping[str, DType] | Schema | Sequence[str] | None,
     ) -> Self:
         from narwhals.schema import Schema
@@ -217,9 +214,7 @@ class PandasLikeDataFrame(
     def __narwhals_namespace__(self) -> PandasLikeNamespace:
         from narwhals._pandas_like.namespace import PandasLikeNamespace
 
-        return PandasLikeNamespace(
-            self._implementation, self._backend_version, version=self._version
-        )
+        return PandasLikeNamespace(self._implementation, version=self._version)
 
     def __native_namespace__(self) -> ModuleType:
         if self._implementation in {
@@ -239,7 +234,6 @@ class PandasLikeDataFrame(
         return self.__class__(
             self.native,
             implementation=self._implementation,
-            backend_version=self._backend_version,
             version=version,
             validate_column_names=False,
         )
@@ -248,7 +242,6 @@ class PandasLikeDataFrame(
         return self.__class__(
             df,
             implementation=self._implementation,
-            backend_version=self._backend_version,
             version=self._version,
             validate_column_names=validate_column_names,
         )
@@ -262,12 +255,7 @@ class PandasLikeDataFrame(
             msg = f"Expected object of length {len_idx}, got: {len_other}."
             raise ShapeError(msg)
         if other.native.index is not index:
-            return set_index(
-                other.native,
-                index,
-                implementation=other._implementation,
-                backend_version=other._backend_version,
-            )
+            return set_index(other.native, index, implementation=other._implementation)
         return other.native
 
     @property
@@ -390,12 +378,7 @@ class PandasLikeDataFrame(
     # --- reshape ---
     def simple_select(self, *column_names: str) -> Self:
         return self._with_native(
-            select_columns_by_name(
-                self.native,
-                list(column_names),
-                self._backend_version,
-                self._implementation,
-            ),
+            select_columns_by_name(self.native, list(column_names), self._implementation),
             validate_column_names=False,
         )
 
@@ -476,12 +459,7 @@ class PandasLikeDataFrame(
 
     def rename(self, mapping: Mapping[str, str]) -> Self:
         return self._with_native(
-            rename(
-                self.native,
-                columns=mapping,
-                implementation=self._implementation,
-                backend_version=self._backend_version,
-            )
+            rename(self.native, columns=mapping, implementation=self._implementation)
         )
 
     def drop(self, columns: Sequence[str], *, strict: bool) -> Self:
@@ -511,43 +489,35 @@ class PandasLikeDataFrame(
             return PandasLikeDataFrame(
                 self.native,
                 implementation=self._implementation,
-                backend_version=self._backend_version,
                 version=self._version,
                 validate_column_names=False,
             )
 
         if backend is Implementation.PANDAS:
-            import pandas as pd  # ignore-banned-import
-
-            return PandasLikeDataFrame(
-                self.to_pandas(),
-                implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd),
-                version=self._version,
-                validate_column_names=False,
-            )
+            kwds: dict[str, Any] = {
+                "implementation": Implementation.PANDAS,
+                "version": self._version,
+                "validate_column_names": False,
+            }
+            if backend is not self._implementation:
+                kwds.update(validate_backend_version=True)
+            return PandasLikeDataFrame(self.to_pandas(), **kwds)
 
         if backend is Implementation.PYARROW:
-            import pyarrow as pa  # ignore-banned-import
-
             from narwhals._arrow.dataframe import ArrowDataFrame
 
             return ArrowDataFrame(
                 native_dataframe=self.to_arrow(),
-                backend_version=parse_version(pa),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=False,
             )
 
         if backend is Implementation.POLARS:
-            import polars as pl  # ignore-banned-import
-
             from narwhals._polars.dataframe import PolarsDataFrame
 
             return PolarsDataFrame(
-                df=self.to_polars(),
-                backend_version=parse_version(pl),
-                version=self._version,
+                df=self.to_polars(), validate_backend_version=True, version=self._version
             )
 
         msg = f"Unsupported `backend` value: {backend}"  # pragma: no cover
@@ -681,18 +651,14 @@ class PandasLikeDataFrame(
         Notice that a native object is returned.
         """
         implementation = self._implementation
-        backend_version = self._backend_version
-
         return rename(
             select_columns_by_name(
                 other.native,
                 column_names=columns_to_select,
-                backend_version=backend_version,
                 implementation=implementation,
             ),
             columns=columns_mapping,
             implementation=implementation,
-            backend_version=backend_version,
         ).drop_duplicates()
 
     def join(
@@ -783,8 +749,6 @@ class PandasLikeDataFrame(
 
     # --- lazy-only ---
     def lazy(self, *, backend: Implementation | None = None) -> CompliantLazyFrameAny:
-        from narwhals.utils import parse_version
-
         pandas_df = self.to_pandas()
         if backend is None:
             return self
@@ -795,7 +759,7 @@ class PandasLikeDataFrame(
 
             return DuckDBLazyFrame(
                 df=duckdb.table("pandas_df"),
-                backend_version=parse_version(duckdb),
+                validate_backend_version=True,
                 version=self._version,
             )
         elif backend is Implementation.POLARS:
@@ -805,18 +769,17 @@ class PandasLikeDataFrame(
 
             return PolarsLazyFrame(
                 df=pl.from_pandas(pandas_df).lazy(),
-                backend_version=parse_version(pl),
+                validate_backend_version=True,
                 version=self._version,
             )
         elif backend is Implementation.DASK:
-            import dask  # ignore-banned-import
             import dask.dataframe as dd  # ignore-banned-import
 
             from narwhals._dask.dataframe import DaskLazyFrame
 
             return DaskLazyFrame(
                 native_dataframe=dd.from_pandas(pandas_df),
-                backend_version=parse_version(dask),
+                validate_backend_version=True,
                 version=self._version,
             )
         elif backend.is_ibis():
@@ -826,7 +789,7 @@ class PandasLikeDataFrame(
 
             return IbisLazyFrame(
                 ibis.memtable(pandas_df, columns=self.columns),
-                backend_version=parse_version(ibis),
+                validate_backend_version=True,
                 version=self._version,
             )
         raise AssertionError  # pragma: no cover
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index 4280a0aa..5705eedc 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
     from narwhals._expression_parsing import ExprMetadata
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
     from narwhals._pandas_like.namespace import PandasLikeNamespace
-    from narwhals._utils import Implementation, Version, _FullContext
+    from narwhals._utils import Implementation, Version, _LimitedContext
     from narwhals.typing import PythonLiteral
 
 WINDOW_FUNCTIONS_TO_PANDAS_EQUIVALENT = {
@@ -116,7 +116,6 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
         evaluate_output_names: EvalNames[PandasLikeDataFrame],
         alias_output_names: AliasNames | None,
         implementation: Implementation,
-        backend_version: tuple[int, ...],
         version: Version,
         scalar_kwargs: ScalarKwargs | None = None,
     ) -> None:
@@ -126,7 +125,6 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
         self._evaluate_output_names = evaluate_output_names
         self._alias_output_names = alias_output_names
         self._implementation = implementation
-        self._backend_version = backend_version
         self._version = version
         self._scalar_kwargs = scalar_kwargs or {}
         self._metadata: ExprMetadata | None = None
@@ -134,9 +132,7 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
     def __narwhals_namespace__(self) -> PandasLikeNamespace:
         from narwhals._pandas_like.namespace import PandasLikeNamespace
 
-        return PandasLikeNamespace(
-            self._implementation, self._backend_version, version=self._version
-        )
+        return PandasLikeNamespace(self._implementation, version=self._version)
 
     def __narwhals_expr__(self) -> None: ...
 
@@ -146,7 +142,7 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
         evaluate_column_names: EvalNames[PandasLikeDataFrame],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         function_name: str = "",
     ) -> Self:
         def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
@@ -155,7 +151,6 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
                     PandasLikeSeries(
                         df._native_frame[column_name],
                         implementation=df._implementation,
-                        backend_version=df._backend_version,
                         version=df._version,
                     )
                     for column_name in evaluate_column_names(df)
@@ -172,12 +167,11 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
             evaluate_output_names=evaluate_column_names,
             alias_output_names=None,
             implementation=context._implementation,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
     @classmethod
-    def from_column_indices(cls, *column_indices: int, context: _FullContext) -> Self:
+    def from_column_indices(cls, *column_indices: int, context: _LimitedContext) -> Self:
         def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
             native = df.native
             return [
@@ -192,7 +186,6 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
             evaluate_output_names=cls._eval_names_indices(column_indices),
             alias_output_names=None,
             implementation=context._implementation,
-            backend_version=context._backend_version,
             version=context._version,
         )
 
@@ -299,11 +292,11 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
                         res_native = getattr(rolling, pandas_function_name)()
                 elif function_name.startswith("ewm"):
                     if self._implementation.is_pandas() and (
-                        backend_version := self._backend_version
+                        self._implementation._backend_version()
                     ) < (1, 2):  # pragma: no cover
                         msg = (
                             "Exponentially weighted calculation is not available in over "
-                            f"context for pandas versions older than 1.2.0, found {backend_version}."
+                            f"context for pandas versions older than 1.2.0, found {self._implementation._backend_version()}."
                         )
                         raise NotImplementedError(msg)
                     ewm = grouped[list(output_names)].ewm(**pandas_kwargs)
@@ -350,6 +343,5 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
             implementation=self._implementation,
-            backend_version=self._backend_version,
             version=self._version,
         )
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 0b27de05..b41ff4a3 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -216,7 +216,7 @@ class PandasLikeGroupBy(EagerGroupBy["PandasLikeDataFrame", "PandasLikeExpr", st
             # This may need updating, depending on https://github.com/pandas-dev/pandas/pull/51466/files
             result.reset_index(inplace=True)  # noqa: PD002
             return self.compliant._with_native(
-                select_columns_by_name(result, new_names, backend_version, implementation)
+                select_columns_by_name(result, new_names, implementation)
             ).rename(dict(zip(self._keys, self._output_key_names)))
 
         if self.compliant.native.empty:
@@ -263,9 +263,7 @@ class PandasLikeGroupBy(EagerGroupBy["PandasLikeDataFrame", "PandasLikeExpr", st
         # This may need updating, depending on https://github.com/pandas-dev/pandas/pull/51466/files
         result_complex.reset_index(inplace=True)  # noqa: PD002
         return self.compliant._with_native(
-            select_columns_by_name(
-                result_complex, new_names, backend_version, implementation
-            )
+            select_columns_by_name(result_complex, new_names, implementation)
         ).rename(dict(zip(self._keys, self._output_key_names)))
 
     def __iter__(self) -> Iterator[tuple[Any, PandasLikeDataFrame]]:
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index 79c5c458..f36d90c7 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -64,15 +64,8 @@ class PandasLikeNamespace(
     def selectors(self) -> PandasSelectorNamespace:
         return PandasSelectorNamespace.from_namespace(self)
 
-    # --- not in spec ---
-    def __init__(
-        self,
-        implementation: Implementation,
-        backend_version: tuple[int, ...],
-        version: Version,
-    ) -> None:
+    def __init__(self, implementation: Implementation, version: Version) -> None:
         self._implementation = implementation
-        self._backend_version = backend_version
         self._version = version
 
     def lit(self, value: NonNestedLiteral, dtype: IntoDType | None) -> PandasLikeExpr:
@@ -94,7 +87,6 @@ class PandasLikeNamespace(
             evaluate_output_names=lambda _df: ["literal"],
             alias_output_names=None,
             implementation=self._implementation,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -110,7 +102,6 @@ class PandasLikeNamespace(
             evaluate_output_names=lambda _df: ["len"],
             alias_output_names=None,
             implementation=self._implementation,
-            backend_version=self._backend_version,
             version=self._version,
         )
 
@@ -229,7 +220,6 @@ class PandasLikeNamespace(
                         (s.to_frame() for s in series), how="horizontal"
                     )._native_frame.min(axis=1),
                     implementation=self._implementation,
-                    backend_version=self._backend_version,
                     version=self._version,
                 ).alias(series[0].name)
             ]
@@ -255,7 +245,6 @@ class PandasLikeNamespace(
                         (s.to_frame() for s in series), how="horizontal"
                     ).native.max(axis=1),
                     implementation=self._implementation,
-                    backend_version=self._backend_version,
                     version=self._version,
                 ).alias(series[0].name)
             ]
diff --git a/narwhals/_pandas_like/selectors.py b/narwhals/_pandas_like/selectors.py
index f6b2a733..187645bf 100644
--- a/narwhals/_pandas_like/selectors.py
+++ b/narwhals/_pandas_like/selectors.py
@@ -29,6 +29,5 @@ class PandasSelector(  # type: ignore[misc]
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
             implementation=self._implementation,
-            backend_version=self._backend_version,
             version=self._version,
         )
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index b98c6743..cba03ed9 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -23,12 +23,7 @@ from narwhals._pandas_like.utils import (
     set_index,
 )
 from narwhals._typing_compat import assert_never
-from narwhals._utils import (
-    Implementation,
-    is_list_of,
-    parse_version,
-    validate_backend_version,
-)
+from narwhals._utils import Implementation, is_list_of, parse_version
 from narwhals.dependencies import is_numpy_array_1d, is_pandas_like_series
 from narwhals.exceptions import InvalidOperationError
 
@@ -44,7 +39,7 @@ if TYPE_CHECKING:
     from narwhals._arrow.typing import ChunkedArrayAny
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
     from narwhals._pandas_like.namespace import PandasLikeNamespace
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.dtypes import DType
     from narwhals.typing import (
         ClosedInterval,
@@ -110,19 +105,12 @@ PANDAS_TO_NUMPY_DTYPE_MISSING = {
 
 class PandasLikeSeries(EagerSeries[Any]):
     def __init__(
-        self,
-        native_series: Any,
-        *,
-        implementation: Implementation,
-        backend_version: tuple[int, ...],
-        version: Version,
+        self, native_series: Any, *, implementation: Implementation, version: Version
     ) -> None:
         self._name = native_series.name
         self._native_series = native_series
         self._implementation = implementation
-        self._backend_version = backend_version
         self._version = version
-        validate_backend_version(self._implementation, self._backend_version)
         # Flag which indicates if, in the final step before applying an operation,
         # the single value behind the PandasLikeSeries should be extract and treated
         # as a Scalar. For example, in `nw.col('a') - nw.lit(3)`, the latter would
@@ -144,9 +132,7 @@ class PandasLikeSeries(EagerSeries[Any]):
     def __narwhals_namespace__(self) -> PandasLikeNamespace:
         from narwhals._pandas_like.namespace import PandasLikeNamespace
 
-        return PandasLikeNamespace(
-            self._implementation, self._backend_version, self._version
-        )
+        return PandasLikeNamespace(self._implementation, self._version)
 
     def _gather(self, rows: SizedMultiIndexSelector[pd.Series[Any]]) -> Self:
         rows = list(rows) if isinstance(rows, tuple) else rows
@@ -159,18 +145,12 @@ class PandasLikeSeries(EagerSeries[Any]):
 
     def _with_version(self, version: Version) -> Self:
         return self.__class__(
-            self.native,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-            version=version,
+            self.native, implementation=self._implementation, version=version
         )
 
     def _with_native(self, series: Any, *, preserve_broadcast: bool = False) -> Self:
         result = self.__class__(
-            series,
-            implementation=self._implementation,
-            backend_version=self._backend_version,
-            version=self._version,
+            series, implementation=self._implementation, version=self._version
         )
         if preserve_broadcast:
             result._broadcast = self._broadcast
@@ -181,20 +161,17 @@ class PandasLikeSeries(EagerSeries[Any]):
         cls,
         data: Iterable[Any],
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         name: str = "",
         dtype: IntoDType | None = None,
         index: Any = None,
     ) -> Self:
         implementation = context._implementation
-        backend_version = context._backend_version
         version = context._version
         ns = implementation.to_native_namespace()
         kwds: dict[str, Any] = {}
         if dtype:
-            kwds["dtype"] = narwhals_to_native_dtype(
-                dtype, None, implementation, backend_version, version
-            )
+            kwds["dtype"] = narwhals_to_native_dtype(dtype, None, implementation, version)
         else:
             if implementation.is_pandas():
                 kwds["copy"] = False
@@ -207,16 +184,11 @@ class PandasLikeSeries(EagerSeries[Any]):
         return is_pandas_like_series(obj)  # pragma: no cover
 
     @classmethod
-    def from_native(cls, data: Any, /, *, context: _FullContext) -> Self:
-        return cls(
-            data,
-            implementation=context._implementation,
-            backend_version=context._backend_version,
-            version=context._version,
-        )
+    def from_native(cls, data: Any, /, *, context: _LimitedContext) -> Self:
+        return cls(data, implementation=context._implementation, version=context._version)
 
     @classmethod
-    def from_numpy(cls, data: Into1DArray, /, *, context: _FullContext) -> Self:
+    def from_numpy(cls, data: Into1DArray, /, *, context: _LimitedContext) -> Self:
         implementation = context._implementation
         arr = data if is_numpy_array_1d(data) else [data]
         native = implementation.to_native_namespace().Series(arr, name="")
@@ -236,12 +208,7 @@ class PandasLikeSeries(EagerSeries[Any]):
                 )
                 compliant = s._with_native(native)
             elif s.native.index is not idx:
-                native = set_index(
-                    s.native,
-                    idx,
-                    implementation=s._implementation,
-                    backend_version=s._backend_version,
-                )
+                native = set_index(s.native, idx, implementation=s._implementation)
                 compliant = s._with_native(native)
             else:
                 compliant = s
@@ -309,7 +276,6 @@ class PandasLikeSeries(EagerSeries[Any]):
                 values.native,
                 self.native.index[indices],
                 implementation=self._implementation,
-                backend_version=self._backend_version,
             )
         s = self.native.copy(deep=True)
         s.iloc[indices] = values
@@ -324,7 +290,6 @@ class PandasLikeSeries(EagerSeries[Any]):
             values.native,
             self.native.index[indices.native],
             implementation=implementation,
-            backend_version=backend_version,
         )
         if implementation is Implementation.PANDAS and parse_version(np) < (2,):
             values_native = values_native.copy()  # pragma: no cover
@@ -339,7 +304,6 @@ class PandasLikeSeries(EagerSeries[Any]):
             dtype,
             dtype_backend=get_dtype_backend(self.native.dtype, self._implementation),
             implementation=self._implementation,
-            backend_version=self._backend_version,
             version=self._version,
         )
         return self._with_native(self.native.astype(pd_dtype), preserve_broadcast=True)
@@ -362,7 +326,6 @@ class PandasLikeSeries(EagerSeries[Any]):
         return PandasLikeDataFrame(
             self.native.to_frame(),
             implementation=self._implementation,
-            backend_version=self._backend_version,
             version=self._version,
             validate_column_names=False,
         )
@@ -679,11 +642,7 @@ class PandasLikeSeries(EagerSeries[Any]):
         dtype_backend = get_dtype_backend(self.native.dtype, self._implementation)
         dtype = (
             narwhals_to_native_dtype(
-                return_dtype,
-                dtype_backend,
-                self._implementation,
-                self._backend_version,
-                self._version,
+                return_dtype, dtype_backend, self._implementation, self._version
             )
             if return_dtype
             else None
@@ -712,12 +671,7 @@ class PandasLikeSeries(EagerSeries[Any]):
     def alias(self, name: str | Hashable) -> Self:
         if name != self.name:
             return self._with_native(
-                rename(
-                    self.native,
-                    name,
-                    implementation=self._implementation,
-                    backend_version=self._backend_version,
-                ),
+                rename(self.native, name, implementation=self._implementation),
                 preserve_broadcast=True,
             )
         return self
@@ -847,12 +801,9 @@ class PandasLikeSeries(EagerSeries[Any]):
             *cols, null_col_pd = list(result.columns)
             output_order = [null_col_pd, *cols]
             result = rename(
-                select_columns_by_name(
-                    result, output_order, self._backend_version, self._implementation
-                ),
+                select_columns_by_name(result, output_order, self._implementation),
                 columns={null_col_pd: null_col_pl},
                 implementation=self._implementation,
-                backend_version=self._backend_version,
             )
         return PandasLikeDataFrame.from_native(result, context=self)
 
@@ -1072,11 +1023,7 @@ class PandasLikeSeries(EagerSeries[Any]):
             result_arr = cast("ChunkedArrayAny", pc.logb(ca, base))
             nw_dtype = native_to_narwhals_dtype(result_arr.type, self._version)
             out_dtype = narwhals_to_native_dtype(
-                nw_dtype,
-                "pyarrow",
-                self._implementation,
-                self._backend_version,
-                self._version,
+                nw_dtype, "pyarrow", self._implementation, self._version
             )
             result_native = native_cls(
                 result_arr, dtype=out_dtype, index=native.index, name=native.name
@@ -1106,11 +1053,7 @@ class PandasLikeSeries(EagerSeries[Any]):
             result_arr = cast("ChunkedArrayAny", pc.exp(ca))
             nw_dtype = native_to_narwhals_dtype(result_arr.type, self._version)
             out_dtype = narwhals_to_native_dtype(
-                nw_dtype,
-                "pyarrow",
-                self._implementation,
-                self._backend_version,
-                self._version,
+                nw_dtype, "pyarrow", self._implementation, self._version
             )
             result_native = native_cls(
                 result_arr, dtype=out_dtype, index=native.index, name=native.name
diff --git a/narwhals/_pandas_like/series_list.py b/narwhals/_pandas_like/series_list.py
index 7816c1b7..3108daf8 100644
--- a/narwhals/_pandas_like/series_list.py
+++ b/narwhals/_pandas_like/series_list.py
@@ -27,7 +27,6 @@ class PandasLikeSeriesListNamespace(
             self.version.dtypes.UInt32(),
             get_dtype_backend(result.dtype, implementation),
             implementation,
-            backend_version,
             self.version,
         )
         return self.with_native(result.astype(dtype)).alias(self.native.name)
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 10980a92..ef345b6f 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -119,12 +119,7 @@ def align_and_extract_native(
         if rhs.native.index is not lhs_index:
             return (
                 lhs.native,
-                set_index(
-                    rhs.native,
-                    lhs_index,
-                    implementation=rhs._implementation,
-                    backend_version=rhs._backend_version,
-                ),
+                set_index(rhs.native, lhs_index, implementation=rhs._implementation),
             )
         return (lhs.native, rhs.native)
 
@@ -136,11 +131,7 @@ def align_and_extract_native(
 
 
 def set_index(
-    obj: NativeNDFrameT,
-    index: Any,
-    *,
-    implementation: Implementation,
-    backend_version: tuple[int, ...],
+    obj: NativeNDFrameT, index: Any, *, implementation: Implementation
 ) -> NativeNDFrameT:
     """Wrapper around pandas' set_axis to set object index.
 
@@ -156,7 +147,7 @@ def set_index(
         obj.index = index
         return obj
     if implementation is Implementation.PANDAS and (
-        (1, 5) <= backend_version < (3,)
+        (1, 5) <= implementation._backend_version() < (3,)
     ):  # pragma: no cover
         return obj.set_axis(index, axis=0, copy=False)
     else:  # pragma: no cover
@@ -164,15 +155,11 @@ def set_index(
 
 
 def rename(
-    obj: NativeNDFrameT,
-    *args: Any,
-    implementation: Implementation,
-    backend_version: tuple[int, ...],
-    **kwargs: Any,
+    obj: NativeNDFrameT, *args: Any, implementation: Implementation, **kwargs: Any
 ) -> NativeNDFrameT:
     """Wrapper around pandas' rename so that we can set `copy` based on implementation/version."""
     if implementation is Implementation.PANDAS and (
-        backend_version >= (3,)
+        implementation._backend_version() >= (3,)
     ):  # pragma: no cover
         return obj.rename(*args, **kwargs, inplace=False)
     return obj.rename(*args, **kwargs, copy=False, inplace=False)
@@ -376,7 +363,6 @@ def narwhals_to_native_dtype(  # noqa: C901, PLR0912, PLR0915
     dtype: IntoDType,
     dtype_backend: DTypeBackend,
     implementation: Implementation,
-    backend_version: tuple[int, ...],
     version: Version,
 ) -> str | PandasDtype:
     if dtype_backend is not None and dtype_backend not in {"pyarrow", "numpy_nullable"}:
@@ -463,6 +449,7 @@ def narwhals_to_native_dtype(  # noqa: C901, PLR0912, PLR0915
         # or at least, convert_dtypes(dtype_backend='pyarrow') doesn't
         # convert to it?
         return "category"
+    backend_version = implementation._backend_version()
     if isinstance_or_issubclass(dtype, dtypes.Datetime):
         # Pandas does not support "ms" or "us" time units before version 2.0
         if implementation is Implementation.PANDAS and backend_version < (
@@ -590,7 +577,6 @@ def calculate_timestamp_date(s: NativeSeriesT, time_unit: str) -> NativeSeriesT:
 def select_columns_by_name(
     df: NativeDataFrameT,
     column_names: list[str] | _1DArray,  # NOTE: Cannot be a tuple!
-    backend_version: tuple[int, ...],
     implementation: Implementation,
 ) -> NativeDataFrameT | Any:
     """Select columns by name.
@@ -601,7 +587,8 @@ def select_columns_by_name(
     if len(column_names) == df.shape[1] and (df.columns == column_names).all():
         return df
     if (df.columns.dtype.kind == "b") or (
-        implementation is Implementation.PANDAS and backend_version < (1, 5)
+        implementation is Implementation.PANDAS
+        and implementation._backend_version() < (1, 5)
     ):
         # See https://github.com/narwhals-dev/narwhals/issues/1349#issuecomment-2470118122
         # for why we need this
@@ -640,15 +627,4 @@ def import_array_module(implementation: Implementation, /) -> ModuleType:
         raise AssertionError(msg)
 
 
-class PandasLikeSeriesNamespace(EagerSeriesNamespace["PandasLikeSeries", Any]):
-    @property
-    def implementation(self) -> Implementation:
-        return self.compliant._implementation
-
-    @property
-    def backend_version(self) -> tuple[int, ...]:
-        return self.compliant._backend_version
-
-    @property
-    def version(self) -> Version:
-        return self.compliant._version
+class PandasLikeSeriesNamespace(EagerSeriesNamespace["PandasLikeSeries", Any]): ...
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
index 1caffea6..1d4543c6 100644
--- a/narwhals/_polars/dataframe.py
+++ b/narwhals/_polars/dataframe.py
@@ -24,9 +24,7 @@ from narwhals._utils import (
     is_slice_index,
     is_slice_none,
     parse_columns_to_drop,
-    parse_version,
     requires,
-    validate_backend_version,
 )
 from narwhals.dependencies import is_numpy_array_1d
 from narwhals.exceptions import ColumnNotFoundError
@@ -43,7 +41,7 @@ if TYPE_CHECKING:
     from narwhals._polars.expr import PolarsExpr
     from narwhals._polars.group_by import PolarsGroupBy, PolarsLazyGroupBy
     from narwhals._translate import IntoArrowTable
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.dataframe import DataFrame, LazyFrame
     from narwhals.dtypes import DType
     from narwhals.schema import Schema
@@ -112,14 +110,31 @@ class PolarsBaseFrame(Generic[NativePolarsFrame]):
     unique: Method[Self]
     with_columns: Method[Self]
 
+    _implementation = Implementation.POLARS
+
     def __init__(
-        self, df: NativePolarsFrame, *, backend_version: tuple[int, ...], version: Version
+        self,
+        df: NativePolarsFrame,
+        *,
+        version: Version,
+        validate_backend_version: bool = False,
     ) -> None:
         self._native_frame: NativePolarsFrame = df
-        self._backend_version = backend_version
-        self._implementation = Implementation.POLARS
         self._version = version
-        validate_backend_version(self._implementation, self._backend_version)
+        if validate_backend_version:
+            self._validate_backend_version()
+
+    def _validate_backend_version(self) -> None:
+        """Raise if installed version below `nw._utils.MIN_VERSIONS`.
+
+        **Only use this when moving between backends.**
+        Otherwise, the validation will have taken place already.
+        """
+        _ = self._implementation._backend_version()
+
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
 
     @property
     def native(self) -> NativePolarsFrame:
@@ -130,9 +145,7 @@ class PolarsBaseFrame(Generic[NativePolarsFrame]):
         return self.native.columns
 
     def __narwhals_namespace__(self) -> PolarsNamespace:
-        return PolarsNamespace(
-            backend_version=self._backend_version, version=self._version
-        )
+        return PolarsNamespace(version=self._version)
 
     def __native_namespace__(self) -> ModuleType:
         if self._implementation is Implementation.POLARS:
@@ -142,20 +155,14 @@ class PolarsBaseFrame(Generic[NativePolarsFrame]):
         raise AssertionError(msg)
 
     def _with_native(self, df: NativePolarsFrame) -> Self:
-        return self.__class__(
-            df, backend_version=self._backend_version, version=self._version
-        )
+        return self.__class__(df, version=self._version)
 
     def _with_version(self, version: Version) -> Self:
-        return self.__class__(
-            self.native, backend_version=self._backend_version, version=version
-        )
+        return self.__class__(self.native, version=version)
 
     @classmethod
-    def from_native(cls, data: NativePolarsFrame, /, *, context: _FullContext) -> Self:
-        return cls(
-            data, backend_version=context._backend_version, version=context._version
-        )
+    def from_native(cls, data: NativePolarsFrame, /, *, context: _LimitedContext) -> Self:
+        return cls(data, version=context._version)
 
     def _check_columns_exist(self, subset: Sequence[str]) -> ColumnNotFoundError | None:
         return check_columns_exist(  # pragma: no cover
@@ -171,7 +178,7 @@ class PolarsBaseFrame(Generic[NativePolarsFrame]):
     @property
     def schema(self) -> dict[str, DType]:
         return {
-            name: native_to_narwhals_dtype(dtype, self._version, self._backend_version)
+            name: native_to_narwhals_dtype(dtype, self._version)
             for name, dtype in self.native.schema.items()
         }
 
@@ -220,21 +227,12 @@ class PolarsBaseFrame(Generic[NativePolarsFrame]):
         )
 
     def collect_schema(self) -> dict[str, DType]:
-        if self._backend_version < (1,):
-            return {
-                name: native_to_narwhals_dtype(
-                    dtype, self._version, self._backend_version
-                )
-                for name, dtype in self.native.schema.items()
-            }
-        else:
-            collected_schema = self.native.collect_schema()
-            return {
-                name: native_to_narwhals_dtype(
-                    dtype, self._version, self._backend_version
-                )
-                for name, dtype in collected_schema.items()
-            }
+        df = self.native
+        schema = df.schema if self._backend_version < (1,) else df.collect_schema()
+        return {
+            name: native_to_narwhals_dtype(dtype, self._version)
+            for name, dtype in schema.items()
+        }
 
     def with_row_index(self, name: str, order_by: Sequence[str] | None) -> Self:
         frame = self.native
@@ -271,10 +269,10 @@ class PolarsDataFrame(PolarsBaseFrame[pl.DataFrame]):
     _evaluate_aliases: Any
 
     @classmethod
-    def from_arrow(cls, data: IntoArrowTable, /, *, context: _FullContext) -> Self:
-        if context._backend_version >= (1, 3):
+    def from_arrow(cls, data: IntoArrowTable, /, *, context: _LimitedContext) -> Self:
+        if context._implementation._backend_version() >= (1, 3):
             native = pl.DataFrame(data)
-        else:
+        else:  # pragma: no cover
             native = cast("pl.DataFrame", pl.from_arrow(_into_arrow_table(data, context)))
         return cls.from_native(native, context=context)
 
@@ -284,7 +282,7 @@ class PolarsDataFrame(PolarsBaseFrame[pl.DataFrame]):
         data: Mapping[str, Any],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         schema: Mapping[str, DType] | Schema | None,
     ) -> Self:
         from narwhals.schema import Schema
@@ -302,7 +300,7 @@ class PolarsDataFrame(PolarsBaseFrame[pl.DataFrame]):
         data: _2DArray,
         /,
         *,
-        context: _FullContext,  # NOTE: Maybe only `Implementation`?
+        context: _LimitedContext,  # NOTE: Maybe only `Implementation`?
         schema: Mapping[str, DType] | Schema | Sequence[str] | None,
     ) -> Self:
         from narwhals.schema import Schema
@@ -358,7 +356,7 @@ class PolarsDataFrame(PolarsBaseFrame[pl.DataFrame]):
                 msg = f"{e!s}\n\nHint: Did you mean one of these columns: {self.columns}?"
                 raise ColumnNotFoundError(msg) from e
             except Exception as e:  # noqa: BLE001
-                raise catch_polars_exception(e, self._backend_version) from None
+                raise catch_polars_exception(e) from None
 
         return func
 
@@ -462,19 +460,16 @@ class PolarsDataFrame(PolarsBaseFrame[pl.DataFrame]):
             # NOTE: (F841) is a false positive
             df = self.native  # noqa: F841
             return DuckDBLazyFrame(
-                duckdb.table("df"),
-                backend_version=parse_version(duckdb),
-                version=self._version,
+                duckdb.table("df"), validate_backend_version=True, version=self._version
             )
         elif backend is Implementation.DASK:
-            import dask  # ignore-banned-import
             import dask.dataframe as dd  # ignore-banned-import
 
             from narwhals._dask.dataframe import DaskLazyFrame
 
             return DaskLazyFrame(
                 dd.from_pandas(self.native.to_pandas()),
-                backend_version=parse_version(dask),
+                validate_backend_version=True,
                 version=self._version,
             )
         elif backend.is_ibis():
@@ -484,9 +479,10 @@ class PolarsDataFrame(PolarsBaseFrame[pl.DataFrame]):
 
             return IbisLazyFrame(
                 ibis.memtable(self.native, columns=self.columns),
-                backend_version=parse_version(ibis),
+                validate_backend_version=True,
                 version=self._version,
             )
+
         raise AssertionError  # pragma: no cover
 
     @overload
@@ -538,7 +534,7 @@ class PolarsDataFrame(PolarsBaseFrame[pl.DataFrame]):
                 separator=separator,
             )
         except Exception as e:  # noqa: BLE001
-            raise catch_polars_exception(e, self._backend_version) from None
+            raise catch_polars_exception(e) from None
         return self._from_native_object(result)
 
     def to_polars(self) -> pl.DataFrame:
@@ -558,7 +554,7 @@ class PolarsDataFrame(PolarsBaseFrame[pl.DataFrame]):
                 other=other, how=how, left_on=left_on, right_on=right_on, suffix=suffix
             )
         except Exception as e:  # noqa: BLE001
-            raise catch_polars_exception(e, self._backend_version) from None
+            raise catch_polars_exception(e) from None
 
 
 class PolarsLazyFrame(PolarsBaseFrame[pl.LazyFrame]):
@@ -601,7 +597,7 @@ class PolarsLazyFrame(PolarsBaseFrame[pl.LazyFrame]):
         try:
             return super().collect_schema()
         except Exception as e:  # noqa: BLE001
-            raise catch_polars_exception(e, self._backend_version) from None
+            raise catch_polars_exception(e) from None
 
     def collect(
         self, backend: Implementation | None, **kwargs: Any
@@ -609,32 +605,28 @@ class PolarsLazyFrame(PolarsBaseFrame[pl.LazyFrame]):
         try:
             result = self.native.collect(**kwargs)
         except Exception as e:  # noqa: BLE001
-            raise catch_polars_exception(e, self._backend_version) from None
+            raise catch_polars_exception(e) from None
 
         if backend is None or backend is Implementation.POLARS:
             return PolarsDataFrame.from_native(result, context=self)
 
         if backend is Implementation.PANDAS:
-            import pandas as pd  # ignore-banned-import
-
             from narwhals._pandas_like.dataframe import PandasLikeDataFrame
 
             return PandasLikeDataFrame(
                 result.to_pandas(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=False,
             )
 
         if backend is Implementation.PYARROW:
-            import pyarrow as pa  # ignore-banned-import
-
             from narwhals._arrow.dataframe import ArrowDataFrame
 
             return ArrowDataFrame(
                 result.to_arrow(),
-                backend_version=parse_version(pa),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=False,
             )
diff --git a/narwhals/_polars/expr.py b/narwhals/_polars/expr.py
index 316671a6..5a2dc21f 100644
--- a/narwhals/_polars/expr.py
+++ b/narwhals/_polars/expr.py
@@ -25,15 +25,17 @@ if TYPE_CHECKING:
 
 
 class PolarsExpr:
-    def __init__(
-        self, expr: pl.Expr, version: Version, backend_version: tuple[int, ...]
-    ) -> None:
+    _implementation = Implementation.POLARS
+
+    def __init__(self, expr: pl.Expr, version: Version) -> None:
         self._native_expr = expr
-        self._implementation = Implementation.POLARS
         self._version = version
-        self._backend_version = backend_version
         self._metadata: ExprMetadata | None = None
 
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
+
     @property
     def native(self) -> pl.Expr:
         return self._native_expr
@@ -42,11 +44,11 @@ class PolarsExpr:
         return "PolarsExpr"
 
     def _with_native(self, expr: pl.Expr) -> Self:
-        return self.__class__(expr, self._version, self._backend_version)
+        return self.__class__(expr, self._version)
 
     @classmethod
     def _from_series(cls, series: Any) -> Self:
-        return cls(series.native, series._version, series._backend_version)
+        return cls(series.native, series._version)
 
     def broadcast(self, kind: Literal[ExprKind.AGGREGATION, ExprKind.LITERAL]) -> Self:
         # Let Polars do its thing.
@@ -64,7 +66,7 @@ class PolarsExpr:
         return {name: min_samples}
 
     def cast(self, dtype: IntoDType) -> Self:
-        dtype_pl = narwhals_to_native_dtype(dtype, self._version, self._backend_version)
+        dtype_pl = narwhals_to_native_dtype(dtype, self._version)
         return self._with_native(self.native.cast(dtype_pl))
 
     def ewm_mean(
@@ -144,7 +146,7 @@ class PolarsExpr:
         self, function: Callable[[Any], Any], return_dtype: IntoDType | None
     ) -> Self:
         return_dtype_pl = (
-            narwhals_to_native_dtype(return_dtype, self._version, self._backend_version)
+            narwhals_to_native_dtype(return_dtype, self._version)
             if return_dtype
             else None
         )
@@ -160,7 +162,7 @@ class PolarsExpr:
         return_dtype: IntoDType | None,
     ) -> Self:
         return_dtype_pl = (
-            narwhals_to_native_dtype(return_dtype, self._version, self._backend_version)
+            narwhals_to_native_dtype(return_dtype, self._version)
             if return_dtype
             else None
         )
@@ -222,9 +224,7 @@ class PolarsExpr:
     def __narwhals_namespace__(self) -> PolarsNamespace:  # pragma: no cover
         from narwhals._polars.namespace import PolarsNamespace
 
-        return PolarsNamespace(
-            backend_version=self._backend_version, version=self._version
-        )
+        return PolarsNamespace(version=self._version)
 
     @property
     def dt(self) -> PolarsExprDateTimeNamespace:
diff --git a/narwhals/_polars/namespace.py b/narwhals/_polars/namespace.py
index f5a78623..f3a8ac6c 100644
--- a/narwhals/_polars/namespace.py
+++ b/narwhals/_polars/namespace.py
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
     from narwhals._compliant import CompliantSelectorNamespace, CompliantWhen
     from narwhals._polars.dataframe import Method, PolarsDataFrame, PolarsLazyFrame
     from narwhals._polars.typing import FrameT
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.schema import Schema
     from narwhals.typing import Into1DArray, IntoDType, TimeUnit, _2DArray
 
@@ -36,19 +36,19 @@ class PolarsNamespace:
     #   error: Type argument "PolarsExpr" of "CompliantWhen" must be a subtype of "CompliantExpr[Any, Any]"
     when: Method[CompliantWhen[PolarsDataFrame, PolarsSeries, PolarsExpr]]  # type: ignore[type-var]
 
-    def __init__(self, *, backend_version: tuple[int, ...], version: Version) -> None:
-        self._backend_version = backend_version
-        self._implementation = Implementation.POLARS
+    _implementation = Implementation.POLARS
+
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
+
+    def __init__(self, *, version: Version) -> None:
         self._version = version
 
     def __getattr__(self, attr: str) -> Any:
         def func(*args: Any, **kwargs: Any) -> Any:
             pos, kwds = extract_args_kwargs(args, kwargs)
-            return self._expr(
-                getattr(pl, attr)(*pos, **kwds),
-                version=self._version,
-                backend_version=self._backend_version,
-            )
+            return self._expr(getattr(pl, attr)(*pos, **kwds), version=self._version)
 
         return func
 
@@ -116,32 +116,20 @@ class PolarsNamespace:
         (1, 0, 0), "Please use `col` for columns selection instead."
     )
     def nth(self, *indices: int) -> PolarsExpr:
-        return self._expr(
-            pl.nth(*indices), version=self._version, backend_version=self._backend_version
-        )
+        return self._expr(pl.nth(*indices), version=self._version)
 
     def len(self) -> PolarsExpr:
         if self._backend_version < (0, 20, 5):
-            return self._expr(
-                pl.count().alias("len"), self._version, self._backend_version
-            )
-        return self._expr(pl.len(), self._version, self._backend_version)
+            return self._expr(pl.count().alias("len"), self._version)
+        return self._expr(pl.len(), self._version)
 
     def all_horizontal(self, *exprs: PolarsExpr, ignore_nulls: bool) -> PolarsExpr:
         it = (expr.fill_null(True) for expr in exprs) if ignore_nulls else iter(exprs)  # noqa: FBT003
-        return self._expr(
-            pl.all_horizontal(*(expr.native for expr in it)),
-            self._version,
-            self._backend_version,
-        )
+        return self._expr(pl.all_horizontal(*(expr.native for expr in it)), self._version)
 
     def any_horizontal(self, *exprs: PolarsExpr, ignore_nulls: bool) -> PolarsExpr:
         it = (expr.fill_null(False) for expr in exprs) if ignore_nulls else iter(exprs)  # noqa: FBT003
-        return self._expr(
-            pl.any_horizontal(*(expr.native for expr in it)),
-            self._version,
-            self._backend_version,
-        )
+        return self._expr(pl.any_horizontal(*(expr.native for expr in it)), self._version)
 
     def concat(
         self,
@@ -151,26 +139,16 @@ class PolarsNamespace:
     ) -> PolarsDataFrame | PolarsLazyFrame:
         result = pl.concat((item.native for item in items), how=how)
         if isinstance(result, pl.DataFrame):
-            return self._dataframe(
-                result, backend_version=self._backend_version, version=self._version
-            )
+            return self._dataframe(result, version=self._version)
         return self._lazyframe.from_native(result, context=self)
 
     def lit(self, value: Any, dtype: IntoDType | None) -> PolarsExpr:
         if dtype is not None:
             return self._expr(
-                pl.lit(
-                    value,
-                    dtype=narwhals_to_native_dtype(
-                        dtype, self._version, self._backend_version
-                    ),
-                ),
+                pl.lit(value, dtype=narwhals_to_native_dtype(dtype, self._version)),
                 version=self._version,
-                backend_version=self._backend_version,
             )
-        return self._expr(
-            pl.lit(value), version=self._version, backend_version=self._backend_version
-        )
+        return self._expr(pl.lit(value), version=self._version)
 
     def mean_horizontal(self, *exprs: PolarsExpr) -> PolarsExpr:
         if self._backend_version < (0, 20, 8):
@@ -178,13 +156,10 @@ class PolarsNamespace:
                 pl.sum_horizontal(e._native_expr for e in exprs)
                 / pl.sum_horizontal(1 - e.is_null()._native_expr for e in exprs),
                 version=self._version,
-                backend_version=self._backend_version,
             )
 
         return self._expr(
-            pl.mean_horizontal(e._native_expr for e in exprs),
-            version=self._version,
-            backend_version=self._backend_version,
+            pl.mean_horizontal(e._native_expr for e in exprs), version=self._version
         )
 
     def concat_str(
@@ -218,14 +193,11 @@ class PolarsNamespace:
                     exprs=[s + v for s, v in zip(separators, values)],
                 )
 
-            return self._expr(
-                result, version=self._version, backend_version=self._backend_version
-            )
+            return self._expr(result, version=self._version)
 
         return self._expr(
             pl.concat_str(pl_exprs, separator=separator, ignore_nulls=ignore_nulls),
             version=self._version,
-            backend_version=self._backend_version,
         )
 
     # NOTE: Implementation is too different to annotate correctly (vs other `*SelectorNamespace`)
@@ -241,67 +213,37 @@ class PolarsNamespace:
 
 
 class PolarsSelectorNamespace:
-    def __init__(self, context: _FullContext, /) -> None:
-        self._implementation = context._implementation
-        self._backend_version = context._backend_version
+    _implementation = Implementation.POLARS
+
+    def __init__(self, context: _LimitedContext, /) -> None:
         self._version = context._version
 
     def by_dtype(self, dtypes: Iterable[DType]) -> PolarsExpr:
         native_dtypes = [
-            narwhals_to_native_dtype(
-                dtype, self._version, self._backend_version
-            ).__class__
+            narwhals_to_native_dtype(dtype, self._version).__class__
             if isinstance(dtype, type) and issubclass(dtype, DType)
-            else narwhals_to_native_dtype(dtype, self._version, self._backend_version)
+            else narwhals_to_native_dtype(dtype, self._version)
             for dtype in dtypes
         ]
-        return PolarsExpr(
-            pl.selectors.by_dtype(native_dtypes),
-            version=self._version,
-            backend_version=self._backend_version,
-        )
+        return PolarsExpr(pl.selectors.by_dtype(native_dtypes), version=self._version)
 
     def matches(self, pattern: str) -> PolarsExpr:
-        return PolarsExpr(
-            pl.selectors.matches(pattern=pattern),
-            version=self._version,
-            backend_version=self._backend_version,
-        )
+        return PolarsExpr(pl.selectors.matches(pattern=pattern), version=self._version)
 
     def numeric(self) -> PolarsExpr:
-        return PolarsExpr(
-            pl.selectors.numeric(),
-            version=self._version,
-            backend_version=self._backend_version,
-        )
+        return PolarsExpr(pl.selectors.numeric(), version=self._version)
 
     def boolean(self) -> PolarsExpr:
-        return PolarsExpr(
-            pl.selectors.boolean(),
-            version=self._version,
-            backend_version=self._backend_version,
-        )
+        return PolarsExpr(pl.selectors.boolean(), version=self._version)
 
     def string(self) -> PolarsExpr:
-        return PolarsExpr(
-            pl.selectors.string(),
-            version=self._version,
-            backend_version=self._backend_version,
-        )
+        return PolarsExpr(pl.selectors.string(), version=self._version)
 
     def categorical(self) -> PolarsExpr:
-        return PolarsExpr(
-            pl.selectors.categorical(),
-            version=self._version,
-            backend_version=self._backend_version,
-        )
+        return PolarsExpr(pl.selectors.categorical(), version=self._version)
 
     def all(self) -> PolarsExpr:
-        return PolarsExpr(
-            pl.selectors.all(),
-            version=self._version,
-            backend_version=self._backend_version,
-        )
+        return PolarsExpr(pl.selectors.all(), version=self._version)
 
     def datetime(
         self,
@@ -311,5 +253,4 @@ class PolarsSelectorNamespace:
         return PolarsExpr(
             pl.selectors.datetime(time_unit=time_unit, time_zone=time_zone),  # type: ignore[arg-type]
             version=self._version,
-            backend_version=self._backend_version,
         )
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index 333ec6da..0c0dc14b 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -11,7 +11,7 @@ from narwhals._polars.utils import (
     narwhals_to_native_dtype,
     native_to_narwhals_dtype,
 )
-from narwhals._utils import Implementation, requires, validate_backend_version
+from narwhals._utils import Implementation, requires
 from narwhals.dependencies import is_numpy_array_1d
 
 if TYPE_CHECKING:
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
     from narwhals._polars.dataframe import Method, PolarsDataFrame
     from narwhals._polars.expr import PolarsExpr
     from narwhals._polars.namespace import PolarsNamespace
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.dtypes import DType
     from narwhals.series import Series
     from narwhals.typing import Into1DArray, IntoDType, MultiIndexSelector, _1DArray
@@ -115,14 +115,15 @@ INHERITED_METHODS = frozenset(
 
 
 class PolarsSeries:
-    def __init__(
-        self, series: pl.Series, *, backend_version: tuple[int, ...], version: Version
-    ) -> None:
+    _implementation = Implementation.POLARS
+
+    def __init__(self, series: pl.Series, *, version: Version) -> None:
         self._native_series: pl.Series = series
-        self._backend_version = backend_version
-        self._implementation = Implementation.POLARS
         self._version = version
-        validate_backend_version(self._implementation, self._backend_version)
+
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        return self._implementation._backend_version()
 
     def __repr__(self) -> str:  # pragma: no cover
         return "PolarsSeries"
@@ -130,9 +131,7 @@ class PolarsSeries:
     def __narwhals_namespace__(self) -> PolarsNamespace:
         from narwhals._polars.namespace import PolarsNamespace
 
-        return PolarsNamespace(
-            backend_version=self._backend_version, version=self._version
-        )
+        return PolarsNamespace(version=self._version)
 
     def __narwhals_series__(self) -> Self:
         return self
@@ -145,24 +144,19 @@ class PolarsSeries:
         raise AssertionError(msg)
 
     def _with_version(self, version: Version) -> Self:
-        return self.__class__(
-            self.native, backend_version=self._backend_version, version=version
-        )
+        return self.__class__(self.native, version=version)
 
     @classmethod
     def from_iterable(
         cls,
         data: Iterable[Any],
         *,
-        context: _FullContext,
+        context: _LimitedContext,
         name: str = "",
         dtype: IntoDType | None = None,
     ) -> Self:
         version = context._version
-        backend_version = context._backend_version
-        dtype_pl = (
-            narwhals_to_native_dtype(dtype, version, backend_version) if dtype else None
-        )
+        dtype_pl = narwhals_to_native_dtype(dtype, version) if dtype else None
         # NOTE: `Iterable` is fine, annotation is overly narrow
         # https://github.com/pola-rs/polars/blob/82d57a4ee41f87c11ca1b1af15488459727efdd7/py-polars/polars/series/series.py#L332-L333
         native = pl.Series(name=name, values=cast("Sequence[Any]", data), dtype=dtype_pl)
@@ -173,13 +167,11 @@ class PolarsSeries:
         return isinstance(obj, pl.Series)
 
     @classmethod
-    def from_native(cls, data: pl.Series, /, *, context: _FullContext) -> Self:
-        return cls(
-            data, backend_version=context._backend_version, version=context._version
-        )
+    def from_native(cls, data: pl.Series, /, *, context: _LimitedContext) -> Self:
+        return cls(data, version=context._version)
 
     @classmethod
-    def from_numpy(cls, data: Into1DArray, /, *, context: _FullContext) -> Self:
+    def from_numpy(cls, data: Into1DArray, /, *, context: _LimitedContext) -> Self:
         native = pl.Series(data if is_numpy_array_1d(data) else [data])
         return cls.from_native(native, context=context)
 
@@ -187,9 +179,7 @@ class PolarsSeries:
         return self._version.series(self, level="full")
 
     def _with_native(self, series: pl.Series) -> Self:
-        return self.__class__(
-            series, backend_version=self._backend_version, version=self._version
-        )
+        return self.__class__(series, version=self._version)
 
     @overload
     def _from_native_object(self, series: pl.Series) -> Self: ...
@@ -235,9 +225,7 @@ class PolarsSeries:
 
     @property
     def dtype(self) -> DType:
-        return native_to_narwhals_dtype(
-            self.native.dtype, self._version, self._backend_version
-        )
+        return native_to_narwhals_dtype(self.native.dtype, self._version)
 
     @property
     def native(self) -> pl.Series:
@@ -252,7 +240,7 @@ class PolarsSeries:
         return self._from_native_object(self.native.__getitem__(item))
 
     def cast(self, dtype: IntoDType) -> Self:
-        dtype_pl = narwhals_to_native_dtype(dtype, self._version, self._backend_version)
+        dtype_pl = narwhals_to_native_dtype(dtype, self._version)
         return self._with_native(self.native.cast(dtype_pl))
 
     @requires.backend_version((1,))
@@ -265,7 +253,7 @@ class PolarsSeries:
     ) -> Self:
         ser = self.native
         dtype = (
-            narwhals_to_native_dtype(return_dtype, self._version, self._backend_version)
+            narwhals_to_native_dtype(return_dtype, self._version)
             if return_dtype
             else None
         )
@@ -309,7 +297,7 @@ class PolarsSeries:
         try:
             native_is_nan = self.native.is_nan()
         except Exception as e:  # noqa: BLE001
-            raise catch_polars_exception(e, self._backend_version) from None
+            raise catch_polars_exception(e) from None
         if self._backend_version < (1, 18):  # pragma: no cover
             select = pl.when(self.native.is_not_null()).then(native_is_nan)
             return self._with_native(pl.select(select)[self.name])
@@ -474,7 +462,7 @@ class PolarsSeries:
         try:
             return self.native.__contains__(other)
         except Exception as e:  # noqa: BLE001
-            raise catch_polars_exception(e, self._backend_version) from None
+            raise catch_polars_exception(e) from None
 
     def hist(  # noqa: C901, PLR0912
         self,
diff --git a/narwhals/_polars/utils.py b/narwhals/_polars/utils.py
index 40d87020..d2d2677f 100644
--- a/narwhals/_polars/utils.py
+++ b/narwhals/_polars/utils.py
@@ -5,7 +5,12 @@ from typing import TYPE_CHECKING, Any, TypeVar, overload
 
 import polars as pl
 
-from narwhals._utils import Version, _DeferredIterable, isinstance_or_issubclass
+from narwhals._utils import (
+    Implementation,
+    Version,
+    _DeferredIterable,
+    isinstance_or_issubclass,
+)
 from narwhals.exceptions import (
     ColumnNotFoundError,
     ComputeError,
@@ -29,6 +34,8 @@ if TYPE_CHECKING:
         "NativeT", bound="pl.DataFrame | pl.LazyFrame | pl.Series | pl.Expr"
     )
 
+BACKEND_VERSION = Implementation.POLARS._backend_version()
+
 
 @overload
 def extract_native(obj: _StoresNative[NativeT]) -> NativeT: ...
@@ -57,7 +64,7 @@ def extract_args_kwargs(
 
 @lru_cache(maxsize=16)
 def native_to_narwhals_dtype(  # noqa: C901, PLR0912
-    dtype: pl.DataType, version: Version, backend_version: tuple[int, ...]
+    dtype: pl.DataType, version: Version
 ) -> DType:
     dtypes = version.dtypes
     if dtype == pl.Float64:
@@ -115,19 +122,15 @@ def native_to_narwhals_dtype(  # noqa: C901, PLR0912
         )
     if isinstance_or_issubclass(dtype, pl.Struct):
         fields = [
-            dtypes.Field(name, native_to_narwhals_dtype(tp, version, backend_version))
+            dtypes.Field(name, native_to_narwhals_dtype(tp, version))
             for name, tp in dtype
         ]
         return dtypes.Struct(fields)
     if isinstance_or_issubclass(dtype, pl.List):
-        return dtypes.List(
-            native_to_narwhals_dtype(dtype.inner, version, backend_version)
-        )
+        return dtypes.List(native_to_narwhals_dtype(dtype.inner, version))
     if isinstance_or_issubclass(dtype, pl.Array):
-        outer_shape = dtype.width if backend_version < (0, 20, 30) else dtype.size
-        return dtypes.Array(
-            native_to_narwhals_dtype(dtype.inner, version, backend_version), outer_shape
-        )
+        outer_shape = dtype.width if BACKEND_VERSION < (0, 20, 30) else dtype.size
+        return dtypes.Array(native_to_narwhals_dtype(dtype.inner, version), outer_shape)
     if dtype == pl.Decimal:
         return dtypes.Decimal()
     if dtype == pl.Time:
@@ -138,7 +141,7 @@ def native_to_narwhals_dtype(  # noqa: C901, PLR0912
 
 
 def narwhals_to_native_dtype(  # noqa: C901, PLR0912
-    dtype: IntoDType, version: Version, backend_version: tuple[int, ...]
+    dtype: IntoDType, version: Version
 ) -> pl.DataType:
     dtypes = version.dtypes
     if dtype == dtypes.Float64:
@@ -194,27 +197,22 @@ def narwhals_to_native_dtype(  # noqa: C901, PLR0912
     if isinstance_or_issubclass(dtype, dtypes.Duration):
         return pl.Duration(dtype.time_unit)  # type: ignore[arg-type]
     if isinstance_or_issubclass(dtype, dtypes.List):
-        return pl.List(narwhals_to_native_dtype(dtype.inner, version, backend_version))
+        return pl.List(narwhals_to_native_dtype(dtype.inner, version))
     if isinstance_or_issubclass(dtype, dtypes.Struct):
         fields = [
-            pl.Field(
-                field.name,
-                narwhals_to_native_dtype(field.dtype, version, backend_version),
-            )
+            pl.Field(field.name, narwhals_to_native_dtype(field.dtype, version))
             for field in dtype.fields
         ]
         return pl.Struct(fields)
     if isinstance_or_issubclass(dtype, dtypes.Array):  # pragma: no cover
         size = dtype.size
-        kwargs = {"width": size} if backend_version < (0, 20, 30) else {"shape": size}
-        return pl.Array(
-            narwhals_to_native_dtype(dtype.inner, version, backend_version), **kwargs
-        )
+        kwargs = {"width": size} if BACKEND_VERSION < (0, 20, 30) else {"shape": size}
+        return pl.Array(narwhals_to_native_dtype(dtype.inner, version), **kwargs)
     return pl.Unknown()  # pragma: no cover
 
 
-def _is_polars_exception(exception: Exception, backend_version: tuple[int, ...]) -> bool:
-    if backend_version >= (1,):
+def _is_polars_exception(exception: Exception) -> bool:
+    if BACKEND_VERSION >= (1,):
         # Old versions of Polars didn't have PolarsError.
         return isinstance(exception, pl.exceptions.PolarsError)
     # Last attempt, for old Polars versions.
@@ -226,9 +224,7 @@ def _is_cudf_exception(exception: Exception) -> bool:
     return str(exception).startswith("CUDF failure")
 
 
-def catch_polars_exception(
-    exception: Exception, backend_version: tuple[int, ...]
-) -> NarwhalsError | Exception:
+def catch_polars_exception(exception: Exception) -> NarwhalsError | Exception:
     if isinstance(exception, pl.exceptions.ColumnNotFoundError):
         return ColumnNotFoundError(str(exception))
     elif isinstance(exception, pl.exceptions.ShapeError):
@@ -239,7 +235,7 @@ def catch_polars_exception(
         return DuplicateError(str(exception))
     elif isinstance(exception, pl.exceptions.ComputeError):
         return ComputeError(str(exception))
-    if _is_polars_exception(exception, backend_version) or _is_cudf_exception(exception):
+    if _is_polars_exception(exception) or _is_cudf_exception(exception):
         return NarwhalsError(str(exception))  # pragma: no cover
     # Just return exception as-is.
     return exception
diff --git a/narwhals/_spark_like/dataframe.py b/narwhals/_spark_like/dataframe.py
index 010b6fd7..9520ea16 100644
--- a/narwhals/_spark_like/dataframe.py
+++ b/narwhals/_spark_like/dataframe.py
@@ -15,12 +15,11 @@ from narwhals._spark_like.utils import (
 )
 from narwhals._utils import (
     Implementation,
+    ValidateBackendVersion,
     find_stacklevel,
     generate_temporary_column_name,
     not_implemented,
     parse_columns_to_drop,
-    parse_version,
-    validate_backend_version,
 )
 from narwhals.exceptions import InvalidOperationError
 from narwhals.typing import CompliantLazyFrame
@@ -39,7 +38,7 @@ if TYPE_CHECKING:
     from narwhals._spark_like.expr import SparkLikeExpr
     from narwhals._spark_like.group_by import SparkLikeLazyGroupBy
     from narwhals._spark_like.namespace import SparkLikeNamespace
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.dataframe import LazyFrame
     from narwhals.dtypes import DType
     from narwhals.typing import JoinStrategy, LazyUniqueKeepStrategy
@@ -53,23 +52,28 @@ Incomplete: TypeAlias = Any  # pragma: no cover
 class SparkLikeLazyFrame(
     CompliantLazyFrame[
         "SparkLikeExpr", "SQLFrameDataFrame", "LazyFrame[SQLFrameDataFrame]"
-    ]
+    ],
+    ValidateBackendVersion,
 ):
     def __init__(
         self,
         native_dataframe: SQLFrameDataFrame,
         *,
-        backend_version: tuple[int, ...],
         version: Version,
         implementation: Implementation,
+        validate_backend_version: bool = False,
     ) -> None:
         self._native_frame: SQLFrameDataFrame = native_dataframe
-        self._backend_version = backend_version
         self._implementation = implementation
         self._version = version
         self._cached_schema: dict[str, DType] | None = None
         self._cached_columns: list[str] | None = None
-        validate_backend_version(self._implementation, self._backend_version)
+        if validate_backend_version:  # pragma: no cover
+            self._validate_backend_version()
+
+    @property
+    def _backend_version(self) -> tuple[int, ...]:  # pragma: no cover
+        return self._implementation._backend_version()
 
     @property
     def _F(self):  # type: ignore[no-untyped-def] # noqa: ANN202, N802
@@ -103,13 +107,8 @@ class SparkLikeLazyFrame(
         return is_native_spark_like(obj)
 
     @classmethod
-    def from_native(cls, data: SQLFrameDataFrame, /, *, context: _FullContext) -> Self:
-        return cls(
-            data,
-            backend_version=context._backend_version,
-            version=context._version,
-            implementation=context._implementation,
-        )
+    def from_native(cls, data: SQLFrameDataFrame, /, *, context: _LimitedContext) -> Self:
+        return cls(data, version=context._version, implementation=context._implementation)
 
     def to_narwhals(self) -> LazyFrame[SQLFrameDataFrame]:
         return self._version.lazyframe(self, level="lazy")
@@ -121,9 +120,7 @@ class SparkLikeLazyFrame(
         from narwhals._spark_like.namespace import SparkLikeNamespace
 
         return SparkLikeNamespace(
-            backend_version=self._backend_version,
-            version=self._version,
-            implementation=self._implementation,
+            version=self._version, implementation=self._implementation
         )
 
     def __narwhals_lazyframe__(self) -> Self:
@@ -131,18 +128,12 @@ class SparkLikeLazyFrame(
 
     def _with_version(self, version: Version) -> Self:
         return self.__class__(
-            self.native,
-            backend_version=self._backend_version,
-            version=version,
-            implementation=self._implementation,
+            self.native, version=version, implementation=self._implementation
         )
 
     def _with_native(self, df: SQLFrameDataFrame) -> Self:
         return self.__class__(
-            df,
-            backend_version=self._backend_version,
-            version=self._version,
-            implementation=self._implementation,
+            df, version=self._version, implementation=self._implementation
         )
 
     def _to_arrow_schema(self) -> pa.Schema:  # pragma: no cover
@@ -213,39 +204,34 @@ class SparkLikeLazyFrame(
         self, backend: ModuleType | Implementation | str | None, **kwargs: Any
     ) -> CompliantDataFrameAny:
         if backend is Implementation.PANDAS:
-            import pandas as pd  # ignore-banned-import
-
             from narwhals._pandas_like.dataframe import PandasLikeDataFrame
 
             return PandasLikeDataFrame(
                 self.native.toPandas(),
                 implementation=Implementation.PANDAS,
-                backend_version=parse_version(pd),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=True,
             )
 
         elif backend is None or backend is Implementation.PYARROW:
-            import pyarrow as pa  # ignore-banned-import
-
             from narwhals._arrow.dataframe import ArrowDataFrame
 
             return ArrowDataFrame(
                 self._collect_to_arrow(),
-                backend_version=parse_version(pa),
+                validate_backend_version=True,
                 version=self._version,
                 validate_column_names=True,
             )
 
         elif backend is Implementation.POLARS:
             import polars as pl  # ignore-banned-import
-            import pyarrow as pa  # ignore-banned-import
 
             from narwhals._polars.dataframe import PolarsDataFrame
 
             return PolarsDataFrame(
                 pl.from_arrow(self._collect_to_arrow()),  # type: ignore[arg-type]
-                backend_version=parse_version(pl),
+                validate_backend_version=True,
                 version=self._version,
             )
 
diff --git a/narwhals/_spark_like/expr.py b/narwhals/_spark_like/expr.py
index c1bbf888..1ee3b86c 100644
--- a/narwhals/_spark_like/expr.py
+++ b/narwhals/_spark_like/expr.py
@@ -20,8 +20,7 @@ from narwhals._spark_like.utils import (
     narwhals_to_native_dtype,
     true_divide,
 )
-from narwhals._utils import Implementation, not_implemented, parse_version
-from narwhals.dependencies import get_pyspark
+from narwhals._utils import Implementation, not_implemented
 
 if TYPE_CHECKING:
     from collections.abc import Iterable, Iterator, Mapping, Sequence
@@ -39,7 +38,7 @@ if TYPE_CHECKING:
     from narwhals._expression_parsing import ExprMetadata
     from narwhals._spark_like.dataframe import SparkLikeLazyFrame
     from narwhals._spark_like.namespace import SparkLikeNamespace
-    from narwhals._utils import Version, _FullContext
+    from narwhals._utils import Version, _LimitedContext
     from narwhals.typing import (
         FillNullStrategy,
         IntoDType,
@@ -70,14 +69,12 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
         *,
         evaluate_output_names: EvalNames[SparkLikeLazyFrame],
         alias_output_names: AliasNames | None,
-        backend_version: tuple[int, ...],
         version: Version,
         implementation: Implementation,
     ) -> None:
         self._call = call
         self._evaluate_output_names = evaluate_output_names
         self._alias_output_names = alias_output_names
-        self._backend_version = backend_version
         self._version = version
         self._implementation = implementation
         self._metadata: ExprMetadata | None = None
@@ -151,13 +148,10 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
     def __narwhals_expr__(self) -> None: ...
 
     def __narwhals_namespace__(self) -> SparkLikeNamespace:  # pragma: no cover
-        # Unused, just for compatibility with PandasLikeExpr
         from narwhals._spark_like.namespace import SparkLikeNamespace
 
         return SparkLikeNamespace(
-            backend_version=self._backend_version,
-            version=self._version,
-            implementation=self._implementation,
+            version=self._version, implementation=self._implementation
         )
 
     def _with_window_function(self, window_function: SparkWindowFunction) -> Self:
@@ -166,7 +160,6 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             window_function,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
@@ -252,7 +245,7 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
         evaluate_column_names: EvalNames[SparkLikeLazyFrame],
         /,
         *,
-        context: _FullContext,
+        context: _LimitedContext,
     ) -> Self:
         def func(df: SparkLikeLazyFrame) -> list[Column]:
             return [df._F.col(col_name) for col_name in evaluate_column_names(df)]
@@ -261,13 +254,12 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             func,
             evaluate_output_names=evaluate_column_names,
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
             implementation=context._implementation,
         )
 
     @classmethod
-    def from_column_indices(cls, *column_indices: int, context: _FullContext) -> Self:
+    def from_column_indices(cls, *column_indices: int, context: _LimitedContext) -> Self:
         def func(df: SparkLikeLazyFrame) -> list[Column]:
             columns = df.columns
             return [df._F.col(columns[i]) for i in column_indices]
@@ -276,7 +268,6 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             func,
             evaluate_output_names=cls._eval_names_indices(column_indices),
             alias_output_names=None,
-            backend_version=context._backend_version,
             version=context._version,
             implementation=context._implementation,
         )
@@ -303,7 +294,6 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             window_function=window_function,
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=context._backend_version,
             version=context._version,
             implementation=context._implementation,
         )
@@ -358,7 +348,6 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             self._callable_to_eval_series(call, **expressifiable_args),
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
@@ -371,7 +360,6 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             self._push_down_window_function(call, **expressifiable_args),
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
@@ -382,7 +370,6 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             self._push_down_window_function(op, other=other),
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
@@ -393,7 +380,6 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             self._window_function,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=func,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
@@ -487,12 +473,10 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
 
     def median(self) -> Self:
         def _median(expr: Column) -> Column:
-            if (
-                self._implementation
-                in {Implementation.PYSPARK, Implementation.PYSPARK_CONNECT}
-                and (pyspark := get_pyspark()) is not None
-                and parse_version(pyspark) < (3, 4)
-            ):  # pragma: no cover
+            if self._implementation in {
+                Implementation.PYSPARK,
+                Implementation.PYSPARK_CONNECT,
+            } and Implementation.PYSPARK._backend_version() < (3, 4):  # pragma: no cover
                 # Use percentile_approx with default accuracy parameter (10000)
                 return self._F.percentile_approx(expr.cast("double"), 0.5)
 
@@ -655,7 +639,6 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
             func,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
diff --git a/narwhals/_spark_like/expr_dt.py b/narwhals/_spark_like/expr_dt.py
index 9577e73a..7c65e4be 100644
--- a/narwhals/_spark_like/expr_dt.py
+++ b/narwhals/_spark_like/expr_dt.py
@@ -142,7 +142,6 @@ class SparkLikeExprDateTimeNamespace(
             func,
             evaluate_output_names=self.compliant._evaluate_output_names,
             alias_output_names=self.compliant._alias_output_names,
-            backend_version=self.compliant._backend_version,
             version=self.compliant._version,
             implementation=self.compliant._implementation,
         )
diff --git a/narwhals/_spark_like/namespace.py b/narwhals/_spark_like/namespace.py
index 2c4e20c8..86c309bb 100644
--- a/narwhals/_spark_like/namespace.py
+++ b/narwhals/_spark_like/namespace.py
@@ -33,14 +33,7 @@ if TYPE_CHECKING:
 class SparkLikeNamespace(
     LazyNamespace[SparkLikeLazyFrame, SparkLikeExpr, "SQLFrameDataFrame"]
 ):
-    def __init__(
-        self,
-        *,
-        backend_version: tuple[int, ...],
-        version: Version,
-        implementation: Implementation,
-    ) -> None:
-        self._backend_version = backend_version
+    def __init__(self, *, version: Version, implementation: Implementation) -> None:
         self._version = version
         self._implementation = implementation
 
@@ -89,7 +82,6 @@ class SparkLikeNamespace(
             call=_lit,
             evaluate_output_names=lambda _df: ["literal"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
@@ -102,7 +94,6 @@ class SparkLikeNamespace(
             func,
             evaluate_output_names=lambda _df: ["len"],
             alias_output_names=None,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
@@ -182,7 +173,6 @@ class SparkLikeNamespace(
 
             return SparkLikeLazyFrame(
                 native_dataframe=reduce(lambda x, y: x.union(y), dfs),
-                backend_version=self._backend_version,
                 version=self._version,
                 implementation=self._implementation,
             )
@@ -192,7 +182,6 @@ class SparkLikeNamespace(
                 native_dataframe=reduce(
                     lambda x, y: x.unionByName(y, allowMissingColumns=True), dfs
                 ),
-                backend_version=self._backend_version,
                 version=self._version,
                 implementation=self._implementation,
             )
@@ -240,7 +229,6 @@ class SparkLikeNamespace(
             call=func,
             evaluate_output_names=combine_evaluate_output_names(*exprs),
             alias_output_names=combine_alias_output_names(*exprs),
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
diff --git a/narwhals/_spark_like/selectors.py b/narwhals/_spark_like/selectors.py
index 013bb9db..38f57932 100644
--- a/narwhals/_spark_like/selectors.py
+++ b/narwhals/_spark_like/selectors.py
@@ -23,7 +23,6 @@ class SparkLikeSelector(CompliantSelector["SparkLikeLazyFrame", "Column"], Spark
             self._call,
             evaluate_output_names=self._evaluate_output_names,
             alias_output_names=self._alias_output_names,
-            backend_version=self._backend_version,
             version=self._version,
             implementation=self._implementation,
         )
diff --git a/narwhals/_utils.py b/narwhals/_utils.py
index 788d82c1..b748b0d0 100644
--- a/narwhals/_utils.py
+++ b/narwhals/_utils.py
@@ -5,7 +5,7 @@ import re
 from collections.abc import Collection, Container, Iterable, Iterator, Mapping, Sequence
 from datetime import timezone
 from enum import Enum, auto
-from functools import lru_cache, wraps
+from functools import cache, lru_cache, wraps
 from importlib.util import find_spec
 from inspect import getattr_static, getdoc
 from secrets import token_hex
@@ -24,7 +24,7 @@ from typing import (
 from warnings import warn
 
 from narwhals._enum import NoAutoEnum
-from narwhals._typing_compat import deprecated
+from narwhals._typing_compat import assert_never, deprecated
 from narwhals.dependencies import (
     get_cudf,
     get_dask_dataframe,
@@ -121,33 +121,6 @@ if TYPE_CHECKING:
     class _SupportsGet(Protocol):  # noqa: PYI046
         def __get__(self, instance: Any, owner: Any | None = None, /) -> Any: ...
 
-    class _StoresImplementation(Protocol):
-        _implementation: Implementation
-        """Implementation of native object (pandas, Polars, PyArrow, ...)."""
-
-    class _StoresBackendVersion(Protocol):
-        _backend_version: tuple[int, ...]
-        """Version tuple for a native package."""
-
-    class _StoresVersion(Protocol):
-        _version: Version
-        """Narwhals API version (V1 or MAIN)."""
-
-    class _LimitedContext(_StoresBackendVersion, _StoresVersion, Protocol):
-        """Provides 2 attributes.
-
-        - `_backend_version`
-        - `_version`
-        """
-
-    class _FullContext(_StoresImplementation, _LimitedContext, Protocol):
-        """Provides 3 attributes.
-
-        - `_implementation`
-        - `_backend_version`
-        - `_version`
-        """
-
     class _StoresColumns(Protocol):
         @property
         def columns(self) -> Sequence[str]: ...
@@ -191,6 +164,52 @@ class _StoresCompliant(Protocol[CompliantT_co]):  # noqa: PYI046
         ...
 
 
+class _StoresBackendVersion(Protocol):
+    @property
+    def _backend_version(self) -> tuple[int, ...]:
+        """Version tuple for a native package."""
+        ...
+
+
+class _StoresVersion(Protocol):
+    _version: Version
+    """Narwhals API version (V1 or MAIN)."""
+
+
+class _StoresImplementation(Protocol):
+    _implementation: Implementation
+    """Implementation of native object (pandas, Polars, PyArrow, ...)."""
+
+
+class _LimitedContext(_StoresImplementation, _StoresVersion, Protocol):
+    """Provides 2 attributes.
+
+    - `_implementation`
+    - `_version`
+    """
+
+
+class _FullContext(_StoresBackendVersion, _LimitedContext, Protocol):
+    """Provides 3 attributes.
+
+    - `_implementation`
+    - `_backend_version`
+    - `_version`
+    """
+
+
+class ValidateBackendVersion(_StoresImplementation, Protocol):
+    """Ensure the target `Implementation` is on a supported version."""
+
+    def _validate_backend_version(self) -> None:
+        """Raise if installed version below `nw._utils.MIN_VERSIONS`.
+
+        **Only use this when moving between backends.**
+        Otherwise, the validation will have taken place already.
+        """
+        _ = self._implementation._backend_version()
+
+
 class Version(Enum):
     V1 = auto()
     MAIN = auto()
@@ -351,8 +370,7 @@ class Implementation(NoAutoEnum):
             msg = "Cannot return native namespace from UNKNOWN Implementation"
             raise AssertionError(msg)
 
-        validate_backend_version(self, self._backend_version())
-
+        self._backend_version()
         module_name = _IMPLEMENTATION_TO_MODULE_NAME.get(self, self.value)
         return _import_native_namespace(module_name)
 
@@ -569,35 +587,8 @@ class Implementation(NoAutoEnum):
         return self is Implementation.SQLFRAME  # pragma: no cover
 
     def _backend_version(self) -> tuple[int, ...]:
-        """Returns backend version.
-
-        As a biproduct of loading the native namespace, we also store it as an attribute
-        under the `_native_namespace` name.
-        """
-        if self is Implementation.UNKNOWN:  # pragma: no cover
-            msg = "Cannot return backend version from UNKNOWN Implementation"
-            raise AssertionError(msg)
-
-        module_name = _IMPLEMENTATION_TO_MODULE_NAME.get(self, self.value)
-        native_namespace = _import_native_namespace(module_name)
-
-        into_version: ModuleType | str
-        if self.is_sqlframe():
-            import sqlframe._version
-
-            into_version = sqlframe._version
-        elif self.is_pyspark() or self.is_pyspark_connect():  # pragma: no cover
-            import pyspark  # ignore-banned-import
-
-            into_version = pyspark
-        elif self.is_dask():
-            import dask  # ignore-banned-import
-
-            into_version = dask
-        else:
-            into_version = native_namespace
-
-        return parse_version(version=into_version)
+        """Returns backend version."""
+        return backend_version(self)
 
 
 MIN_VERSIONS: Mapping[Implementation, tuple[int, ...]] = {
@@ -623,14 +614,6 @@ _IMPLEMENTATION_TO_MODULE_NAME: Mapping[Implementation, str] = {
 """Stores non default mapping from Implementation to module name"""
 
 
-def validate_backend_version(
-    implementation: Implementation, backend_version: tuple[int, ...]
-) -> None:
-    if backend_version < (min_version := MIN_VERSIONS[implementation]):
-        msg = f"Minimum version of {implementation} supported by Narwhals is {min_version}, found: {backend_version}"
-        raise ValueError(msg)
-
-
 @lru_cache(maxsize=16)
 def _import_native_namespace(module_name: str) -> ModuleType:
     from importlib import import_module
@@ -638,6 +621,41 @@ def _import_native_namespace(module_name: str) -> ModuleType:
     return import_module(module_name)
 
 
+# NOTE: We can safely use an unbounded cache, the size is constrained by `len(Implementation._member_names_)`
+# Faster than `lru_cache`
+# https://docs.python.org/3/library/functools.html#functools.cache
+@cache
+def backend_version(implementation: Implementation, /) -> tuple[int, ...]:
+    if not isinstance(implementation, Implementation):
+        assert_never(implementation)
+    if implementation is Implementation.UNKNOWN:  # pragma: no cover
+        msg = "Cannot return backend version from UNKNOWN Implementation"
+        raise AssertionError(msg)
+    into_version: ModuleType | str
+    impl = implementation
+    module_name = _IMPLEMENTATION_TO_MODULE_NAME.get(impl, impl.value)
+    native_namespace = _import_native_namespace(module_name)
+    if impl.is_sqlframe():
+        import sqlframe._version
+
+        into_version = sqlframe._version
+    elif impl.is_pyspark() or impl.is_pyspark_connect():  # pragma: no cover
+        import pyspark  # ignore-banned-import
+
+        into_version = pyspark
+    elif impl.is_dask():
+        import dask  # ignore-banned-import
+
+        into_version = dask
+    else:
+        into_version = native_namespace
+    version = parse_version(into_version)
+    if version < (min_version := MIN_VERSIONS[impl]):
+        msg = f"Minimum version of {impl} supported by Narwhals is {min_version}, found: {version}"
+        raise ValueError(msg)
+    return version
+
+
 def flatten(args: Any) -> list[Any]:
     return list(args[0] if (len(args) == 1 and _is_iterable(args[0])) else args)
 
@@ -1034,7 +1052,6 @@ def maybe_set_index(
             native_obj,
             keys,
             implementation=obj._compliant_series._implementation,  # type: ignore[union-attr]
-            backend_version=obj._compliant_series._backend_version,  # type: ignore[union-attr]
         )
         return df_any._with_compliant(df_any._compliant_series._with_native(native_obj))
     else:
@@ -1716,7 +1733,7 @@ def _remap_full_join_keys(
     return dict(zip(right_on, right_keys_suffixed))
 
 
-def _into_arrow_table(data: IntoArrowTable, context: _FullContext, /) -> pa.Table:
+def _into_arrow_table(data: IntoArrowTable, context: _LimitedContext, /) -> pa.Table:
     """Guards `ArrowDataFrame.from_arrow` w/ safer imports.
 
     Arguments:
@@ -1727,12 +1744,7 @@ def _into_arrow_table(data: IntoArrowTable, context: _FullContext, /) -> pa.Tabl
         A PyArrow Table.
     """
     if find_spec("pyarrow"):
-        import pyarrow as pa  # ignore-banned-import
-
-        from narwhals._arrow.namespace import ArrowNamespace
-
-        version = context._version
-        ns = ArrowNamespace(backend_version=parse_version(pa), version=version)
+        ns = context._version.namespace.from_backend("pyarrow").compliant
         return ns._dataframe.from_arrow(data, context=ns).native
     else:  # pragma: no cover
         msg = f"'pyarrow>=14.0.0' is required for `from_arrow` for object of type {qualified_type_name(data)!r}."
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 09246819..2d7a657f 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -32,7 +32,6 @@ from narwhals._utils import (
     is_sequence_like,
     is_slice_none,
     issue_deprecation_warning,
-    parse_version,
     supports_arrow_c_stream,
 )
 from narwhals.dependencies import get_polars, is_numpy_array
@@ -503,11 +502,11 @@ class DataFrame(BaseFrame[DataFrameT]):
         if supports_arrow_c_stream(native_frame):
             return native_frame.__arrow_c_stream__(requested_schema=requested_schema)
         try:
-            import pyarrow as pa  # ignore-banned-import
+            pa_version = Implementation.PYARROW._backend_version()
         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) < (14, 0):  # pragma: no cover
+        if pa_version < (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/dependencies.py b/narwhals/dependencies.py
index 5f889f94..483605f4 100644
--- a/narwhals/dependencies.py
+++ b/narwhals/dependencies.py
@@ -78,7 +78,7 @@ def get_numpy() -> Any:
     return sys.modules.get("numpy", None)
 
 
-def get_dask() -> Any:
+def get_dask() -> Any:  # pragma: no cover
     """Get dask (if already imported - else return None)."""
     return sys.modules.get("dask", None)
 
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 9e5db828..2110b440 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -25,7 +25,6 @@ from narwhals._utils import (
     is_eager_allowed,
     is_sequence_but_not_str,
     issue_deprecation_warning,
-    parse_version,
     supports_arrow_c_stream,
     validate_laziness,
 )
@@ -563,7 +562,7 @@ def _get_deps_info() -> dict[str, str]:
     Returns:
         Mapping from dependency to version.
     """
-    from importlib.metadata import PackageNotFoundError, version
+    from importlib.metadata import PackageNotFoundError
 
     from narwhals import __version__
 
@@ -780,7 +779,7 @@ def scan_csv(
             csv_reader.load(source)
             if (
                 implementation is Implementation.SQLFRAME
-                and parse_version(version("sqlframe")) < (3, 27, 0)
+                and implementation._backend_version() < (3, 27, 0)
             )
             else csv_reader.options(**kwargs).load(source)
         )
@@ -978,7 +977,7 @@ def scan_parquet(
             pq_reader.load(source)
             if (
                 implementation is Implementation.SQLFRAME
-                and parse_version(version("sqlframe")) < (3, 27, 0)
+                and implementation._backend_version() < (3, 27, 0)
             )
             else pq_reader.options(**kwargs).load(source)
         )
diff --git a/narwhals/schema.py b/narwhals/schema.py
index c5042261..4185a994 100644
--- a/narwhals/schema.py
+++ b/narwhals/schema.py
@@ -10,7 +10,7 @@ from collections import OrderedDict
 from functools import partial
 from typing import TYPE_CHECKING, cast
 
-from narwhals._utils import Implementation, Version, parse_version
+from narwhals._utils import Implementation, Version
 
 if TYPE_CHECKING:
     from collections.abc import Iterable, Mapping
@@ -135,14 +135,11 @@ class Schema(BaseSchema):
             >>> schema.to_pandas("pyarrow")
             {'a': 'Int64[pyarrow]', 'b': 'timestamp[ns][pyarrow]'}
         """
-        import pandas as pd  # ignore-banned-import
-
         from narwhals._pandas_like.utils import narwhals_to_native_dtype
 
         to_native_dtype = partial(
             narwhals_to_native_dtype,
             implementation=Implementation.PANDAS,
-            backend_version=parse_version(pd),
             version=self._version,
         )
         if dtype_backend is None or isinstance(dtype_backend, str):
@@ -192,14 +189,9 @@ class Schema(BaseSchema):
 
         from narwhals._polars.utils import narwhals_to_native_dtype
 
-        pl_version = parse_version(pl)
+        pl_version = Implementation.POLARS._backend_version()
         schema = (
-            (
-                name,
-                narwhals_to_native_dtype(
-                    dtype, self._version, backend_version=pl_version
-                ),
-            )
+            (name, narwhals_to_native_dtype(dtype, self._version))
             for name, dtype in self.items()
         )
         return (
diff --git a/narwhals/series.py b/narwhals/series.py
index 2284dee1..9bbd6ed5 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -5,12 +5,12 @@ from collections.abc import Iterator, Mapping, Sequence
 from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, overload
 
 from narwhals._utils import (
+    Implementation,
     _validate_rolling_arguments,
     ensure_type,
     generate_repr,
     is_compliant_series,
     is_index_selector,
-    parse_version,
     supports_arrow_c_stream,
 )
 from narwhals.dependencies import is_numpy_scalar
@@ -33,7 +33,6 @@ if TYPE_CHECKING:
     from typing_extensions import Self
 
     from narwhals._compliant import CompliantSeries
-    from narwhals._utils import Implementation
     from narwhals.dataframe import DataFrame, MultiIndexSelector
     from narwhals.dtypes import DType
     from narwhals.typing import (
@@ -202,11 +201,11 @@ class Series(Generic[IntoSeriesT]):
         if supports_arrow_c_stream(native_series):
             return native_series.__arrow_c_stream__(requested_schema=requested_schema)
         try:
-            import pyarrow as pa  # ignore-banned-import
+            pa_version = Implementation.PYARROW._backend_version()
         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) < (16, 0):  # pragma: no cover
+        if pa_version < (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)
         from narwhals._arrow.utils import chunked_array
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 0d5a24f0..497874aa 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -12,9 +12,8 @@ from narwhals._namespace import (
     is_native_polars,
     is_native_spark_like,
 )
-from narwhals._utils import Version
+from narwhals._utils import Implementation, Version
 from narwhals.dependencies import (
-    get_dask,
     get_dask_expr,
     get_numpy,
     get_pandas,
@@ -361,7 +360,6 @@ def _from_native_impl(  # noqa: C901, PLR0911, PLR0912, PLR0915
         is_compliant_dataframe,
         is_compliant_lazyframe,
         is_compliant_series,
-        parse_version,
     )
     from narwhals.dataframe import DataFrame, LazyFrame
     from narwhals.series import Series
@@ -480,8 +478,6 @@ def _from_native_impl(  # noqa: C901, PLR0911, PLR0912, PLR0915
 
     # Dask
     elif is_dask_dataframe(native_object):
-        from narwhals._dask.namespace import DaskNamespace
-
         if series_only:
             if not pass_through:
                 msg = "Cannot only use `series_only` with dask DataFrame"
@@ -492,13 +488,15 @@ def _from_native_impl(  # noqa: C901, PLR0911, PLR0912, PLR0915
                 msg = "Cannot only use `eager_only` or `eager_or_interchange_only` with dask DataFrame"
                 raise TypeError(msg)
             return native_object
-        dask_version = parse_version(get_dask())
-        if dask_version <= (2024, 12, 1) and get_dask_expr() is None:  # pragma: no cover
+        if (
+            Implementation.DASK._backend_version() <= (2024, 12, 1)
+            and get_dask_expr() is None
+        ):  # pragma: no cover
             msg = "Please install dask-expr"
             raise ImportError(msg)
         return (
-            DaskNamespace(backend_version=dask_version, version=version)
-            .from_native(native_object)
+            version.namespace.from_backend(Implementation.DASK)
+            .compliant.from_native(native_object)
             .to_narwhals()
         )
 
diff --git a/pyproject.toml b/pyproject.toml
index 7746404f..ffa51e3d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -284,6 +284,7 @@ exclude_also = [
   "if .*implementation.is_pyspark_connect",
   'request.applymarker\(pytest.mark.xfail',
   'backend_version <',
+  '.*._backend_version\(\) <',
   'if ".*" in str\(constructor',
   'pytest.skip\(',
   'assert_never\(',


Test patch:
diff --git a/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
index 67356452..e85c9273 100644
--- a/.github/workflows/downstream_tests.yml
+++ b/.github/workflows/downstream_tests.yml
@@ -86,8 +86,6 @@ jobs:
         run: |
           cd marimo
           . .venv/bin/activate
-          # temporary to get CI green
-          uv pip install "sqlglot<26.32"
           uv pip install -e ".[dev]"
           which python
       - name: install-narwhals-dev
@@ -178,24 +176,35 @@ jobs:
         run: |
           git clone https://github.com/posit-dev/py-shiny.git
           cd py-shiny
+          uv venv -p ${{ matrix.python-version }}
           git log
       - name: install-basics
-        run: uv pip install --upgrade tox virtualenv setuptools --system
+        run: |
+          cd py-shiny
+          . .venv/bin/activate
+          uv pip install --upgrade tox virtualenv setuptools
       - name: install-shiny-dev
-        env:
-          UV_SYSTEM_PYTHON: 1
         run: |
           cd py-shiny
+          . .venv/bin/activate
+          # temporary to get CI green
+          uv pip install "chatlas<0.9.0"
           make narwhals-install-shiny
       - name: install-narwhals-dev
         run: |
-          uv pip uninstall narwhals --system
-          uv pip install -e . --system
+          cd py-shiny
+          . .venv/bin/activate
+          uv pip uninstall narwhals
+          uv pip install -e ./..
       - name: show-deps
-        run: uv pip freeze
+        run: |
+          cd py-shiny
+          . .venv/bin/activate
+          uv pip freeze
       - name: Run `make narwhals-test-integration`
         run: |
           cd py-shiny
+          . .venv/bin/activate
           # Isort seems to behave slightly differently in CI
           # so we ignore its output
           make format -s
@@ -228,8 +237,6 @@ jobs:
         run: |
           cd tea-tasting
           uv sync --group test
-          # temporary to get CI green
-          uv pip install "sqlglot<26.32"
       - name: install-narwhals-dev
         run: |
           cd tea-tasting
@@ -450,8 +457,6 @@ jobs:
           uv venv -p ${{ matrix.python-version }}
           . .venv/bin/activate
           uv pip install . --group dev
-          # temporary to get CI green
-          uv pip install "sqlglot<26.32"
           uv pip install pytest pytest-cov pytest-snapshot pandas polars "ibis-framework[duckdb,mysql,postgres,sqlite]>=9.5.0" chatlas shiny
       - name: install-narwhals-dev
         run: |
diff --git a/tests/dependencies/imports_test.py b/tests/dependencies/imports_test.py
index 58ccb02e..a61e466f 100644
--- a/tests/dependencies/imports_test.py
+++ b/tests/dependencies/imports_test.py
@@ -5,6 +5,7 @@ from types import ModuleType
 
 import pytest
 
+from narwhals._utils import backend_version
 from narwhals.utils import Implementation
 
 implementations = [
@@ -38,9 +39,8 @@ def test_to_native_namespace_min_version(
         reason = f"{impl.value} not installed"
         pytest.skip(reason=reason)
 
-    monkeypatch.setattr(
-        "narwhals._utils.Implementation._backend_version", lambda _: (0, 0, 1)
-    )
+    monkeypatch.setattr("narwhals._utils.parse_version", lambda _: (0, 0, 1))
+    backend_version.cache_clear()
 
     with pytest.raises(ValueError, match="Minimum version"):
         impl.to_native_namespace()
diff --git a/tests/frame/arrow_c_stream_test.py b/tests/frame/arrow_c_stream_test.py
index d5b0df31..402cbd4f 100644
--- a/tests/frame/arrow_c_stream_test.py
+++ b/tests/frame/arrow_c_stream_test.py
@@ -50,3 +50,5 @@ def test_arrow_c_stream_test_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
     result = pa.table(df)
     expected = pa.table({"a": [1, 2, 3]})
     assert pc.all(pc.equal(result["a"], expected["a"])).as_py()
+    result_2 = nw.from_arrow(result, backend="pandas").to_arrow()
+    assert pc.all(pc.equal(result_2["a"], expected["a"])).as_py()