Repository: narwhals-dev/narwhals
Pull request #551 (2024-07-18 21:52:53)

feat: arrow string namespace(s)
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue #372 (but hidden)

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added 
- [x] Documented the changes



Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3d3aafa7..ae34067e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,7 +6,7 @@ Thank you for your interest in contributing to Narwhals! Any kind of improvement
 
 Here's how you can set up your local development environment to contribute:
 
-1. Make sure you have Python3.9+ installed
+1. Make sure you have Python3.8+ installed (for example, Python 3.11)
 2. Create a new virtual environment with `python3.11 -m venv .venv` (or whichever version of Python3.9+ you prefer)
 3. Activate it: `. .venv/bin/activate`
 4. Install Narwhals: `pip install -e .`
@@ -71,5 +71,4 @@ Please remember to abide by the code of conduct, else you'll be conducted away f
 
 We have a community call every 2 weeks, all welcome to attend.
 
-[Subscribe to our calendar](https://calendar.google.com/calendar/embed?src=27ff6dc5f598c1d94c1f6e627a1aaae680e2fac88f848bda1f2c7946ae74d5ab%40group.calendar.google.com&ctz=Europe%2FLondon).
-
+[Subscribe to our calendar](https://calendar.google.com/calendar/embed?src=27ff6dc5f598c1d94c1f6e627a1aaae680e2fac88f848bda1f2c7946ae74d5ab%40group.calendar.google.com).
diff --git a/README.md b/README.md
index 94bc8c66..20f4c06d 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ Get started!
 
 - [Read the documentation](https://narwhals-dev.github.io/narwhals/)
 - [Chat with us on Discord!](https://discord.gg/V3PqtB4VA4)
-- [Join our community call](https://calendar.google.com/calendar/embed?src=27ff6dc5f598c1d94c1f6e627a1aaae680e2fac88f848bda1f2c7946ae74d5ab%40group.calendar.google.com&ctz=Europe%2FLondon)
+- [Join our community call](https://calendar.google.com/calendar/embed?src=27ff6dc5f598c1d94c1f6e627a1aaae680e2fac88f848bda1f2c7946ae74d5ab%40group.calendar.google.com)
 - [Read the contributing guide](https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md)
 
 ## Used by / integrates with
@@ -87,6 +87,18 @@ If you said yes to both, we'd love to hear from you!
 **Note**: You might suspect that this is a secret ploy to infiltrate the Polars API everywhere.
 Indeed, you may suspect that.
 
+## Funding
+
+Narwhals is 100% independent, community-driven, and community-owned.
+We are extremely grateful to the following organisations for having
+provided some funding / development time:
+
+- [Quansight Labs](https://labs.quansight.org/)
+- [OpenTeams](https://www.openteams.com/)
+
+If you contribute to Narwhals on company time, feel free to reach out and we can add your employer
+to the list!
+
 ## Why "Narwhals"?
 
 [Coz they are so awesome](https://youtu.be/ykwqXuMPsoc?si=A-i8LdR38teYsos4).
diff --git a/docs/how_it_works.md b/docs/how_it_works.md
index 9bea4cc6..4bba89e0 100644
--- a/docs/how_it_works.md
+++ b/docs/how_it_works.md
@@ -58,28 +58,8 @@ it to:
 
 Now let's turn our attention to the implementation.
 
-## Polars implementation
-
-For Polars, Narwhals just "passes everything through". For example consider the following:
-```python exec="1"
-import polars as pl
-import narwhals as nw
-
-df_pl = pl.DataFrame({'a': [1,2,3], 'b': [4,5,6]})
-df = nw.from_native(df_pl)
-df.select(nw.col('a')+1)
-```
-
-`nw.col('a')` produces a `narwhals.expression.Expr` object, which has a private `_call` method.
-Inside `DataFrame.select`, we call `nw.col('a')._call(pl)`, which produces `pl.col('a')`.
-
-We then let Polars do its thing. Which is nice, but also not particularly interesting.
-How about translating expressions to pandas? Well, it's
-interesting to us, and you're still reading, so maybe it'll be interesting to you too.
-
 ## pandas implementation
 
-When we called `nw.col('a')._call(pl)`, we got a Narwhals-compliant Polars namespace.
 The pandas namespace (`pd`) isn't Narwhals-compliant, as the pandas API is very different
 from Polars'. So...Narwhals implements a `PandasLikeNamespace`, which includes the top-level
 Polars functions included in the Narwhals API:
@@ -151,6 +131,24 @@ than running pandas directly.
 Further attempts at demistifying Narwhals, refactoring code so it's clearer, and explaining
 this section better are 110% welcome.
 
+## Polars and other implementations
+
+Other implementations are similar to the above: their define their own Narwhals-compliant
+objects. So, all-in-all, there are a couple of layers here:
+
+- `nw.DataFrame` is backed by a Narwhals-compliant Dataframe, such as:
+  - `narwhals._pandas_like.dataframe.PandasLikeDataFrame`
+  - `narwhals._arrow.dataframe.ArrowDataFrame`
+  - `narwhals._polars.dataframe.PolarsDataFrame`
+- each Narwhals-compliant DataFrame is backed by a native Dataframe, for example:
+  - `narwhals._pandas_like.dataframe.PandasLikeDataFrame` is backed by a pandas DataFrame
+  - `narwhals._arrow.dataframe.ArrowDataFrame` is backed by a PyArrow Table
+  - `narwhals._polars.dataframe.PolarsDataFrame` is backed by a Polars DataFrame
+
+Each implementation defines its own objects in subfolders such as `narwhals._pandas_like`,
+`narwhals._arrow`, `narwhals._polars`, whereas the top-level modules such as `narwhals.dataframe`
+and `narwhals.series` coordinate how to dispatch the Narwhals API to each backend.
+
 ## Group-by
 
 Group-by is probably one of Polars' most significant innovations (on the syntax side) with respect
diff --git a/docs/installation.md b/docs/installation.md
index b492bd1a..9fda66d1 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -11,6 +11,6 @@ Then, if you start the Python REPL and see the following:
 ```python
 >>> import narwhals
 >>> narwhals.__version__
-'1.1.1'
+'1.1.2'
 ```
 then installation worked correctly!
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index fa2e9f91..cf657771 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -46,7 +46,7 @@ from narwhals.utils import maybe_align_index
 from narwhals.utils import maybe_convert_dtypes
 from narwhals.utils import maybe_set_index
 
-__version__ = "1.1.1"
+__version__ = "1.1.2"
 
 __all__ = [
     "selectors",
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 94dccd0c..7526e528 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -14,6 +14,7 @@ from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pyarrow
 from narwhals.dependencies import get_pyarrow_parquet
 from narwhals.utils import flatten
+from narwhals.utils import generate_unique_token
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -208,7 +209,7 @@ class ArrowDataFrame:
         self,
         other: Self,
         *,
-        how: Literal["inner"] = "inner",
+        how: Literal["left", "inner", "outer", "cross", "anti", "semi"] = "inner",
         left_on: str | list[str] | None,
         right_on: str | list[str] | None,
     ) -> Self:
@@ -217,24 +218,35 @@ class ArrowDataFrame:
         if isinstance(right_on, str):
             right_on = [right_on]
 
-        if how == "cross":  # type: ignore[comparison-overlap]
-            raise NotImplementedError
-
-        if how == "anti":  # type: ignore[comparison-overlap]
-            raise NotImplementedError
+        how_to_join_map = {
+            "anti": "left anti",
+            "semi": "left semi",
+            "inner": "inner",
+            "left": "left outer",
+        }
 
-        if how == "semi":  # type: ignore[comparison-overlap]
-            raise NotImplementedError
+        if how == "cross":
+            plx = self.__narwhals_namespace__()
+            key_token = generate_unique_token(
+                n_bytes=8, columns=[*self.columns, *other.columns]
+            )
 
-        if how == "left":  # type: ignore[comparison-overlap]
-            raise NotImplementedError
+            return self._from_native_dataframe(
+                self.with_columns(**{key_token: plx.lit(0, None)})._native_dataframe.join(
+                    other.with_columns(**{key_token: plx.lit(0, None)})._native_dataframe,
+                    keys=key_token,
+                    right_keys=key_token,
+                    join_type="inner",
+                    right_suffix="_right",
+                ),
+            ).drop(key_token)
 
         return self._from_native_dataframe(
             self._native_dataframe.join(
                 other._native_dataframe,
                 keys=left_on,
                 right_keys=right_on,
-                join_type=how,
+                join_type=how_to_join_map[how],
                 right_suffix="_right",
             ),
         )
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index f8bfbffc..17398300 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -144,6 +144,9 @@ class ArrowExpr:
     def count(self) -> Self:
         return reuse_series_implementation(self, "count", returns_scalar=True)
 
+    def n_unique(self) -> Self:
+        return reuse_series_implementation(self, "n_unique", returns_scalar=True)
+
     def std(self, ddof: int = 1) -> Self:
         return reuse_series_implementation(self, "std", ddof=ddof, returns_scalar=True)
 
@@ -212,6 +215,9 @@ class ArrowExpr:
             self, "sample", n=n, fraction=fraction, with_replacement=with_replacement
         )
 
+    def fill_null(self: Self, value: Any) -> Self:
+        return reuse_series_implementation(self, "fill_null", value=value)
+
     @property
     def dt(self) -> ArrowExprDateTimeNamespace:
         return ArrowExprDateTimeNamespace(self)
@@ -251,6 +257,40 @@ class ArrowExprStringNamespace:
     def __init__(self, expr: ArrowExpr) -> None:
         self._expr = expr
 
+    def starts_with(self, prefix: str) -> ArrowExpr:
+        return reuse_series_namespace_implementation(
+            self._expr,
+            "str",
+            "starts_with",
+            prefix,
+        )
+
+    def ends_with(self, suffix: str) -> ArrowExpr:
+        return reuse_series_namespace_implementation(
+            self._expr,
+            "str",
+            "ends_with",
+            suffix,
+        )
+
+    def contains(self, pattern: str, *, literal: bool) -> ArrowExpr:
+        return reuse_series_namespace_implementation(
+            self._expr, "str", "contains", pattern, literal=literal
+        )
+
+    def slice(self, offset: int, length: int | None = None) -> ArrowExpr:
+        return reuse_series_namespace_implementation(
+            self._expr, "str", "slice", offset, length
+        )
+
+    def to_datetime(self, format: str | None = None) -> ArrowExpr:  # noqa: A002
+        return reuse_series_namespace_implementation(
+            self._expr,
+            "str",
+            "to_datetime",
+            format,
+        )
+
     def to_uppercase(self) -> ArrowExpr:
         return reuse_series_namespace_implementation(
             self._expr,
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index c287dd81..331d9a4a 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -175,6 +175,12 @@ class ArrowSeries:
         pc = get_pyarrow_compute()
         return pc.count(self._native_series)  # type: ignore[no-any-return]
 
+    def n_unique(self) -> int:
+        pc = get_pyarrow_compute()
+        unique_values = pc.unique(self._native_series)
+        count_unique = pc.count(unique_values, mode="all")
+        return count_unique.as_py()  # type: ignore[no-any-return]
+
     def __narwhals_namespace__(self) -> ArrowNamespace:
         from narwhals._arrow.namespace import ArrowNamespace
 
@@ -314,6 +320,14 @@ class ArrowSeries:
         mask = np.random.choice(idx, size=n, replace=with_replacement)
         return self._from_native_series(pc.take(ser, mask))
 
+    def fill_null(self: Self, value: Any) -> Self:
+        pa = get_pyarrow()
+        pc = get_pyarrow_compute()
+        ser = self._native_series
+        dtype = ser.type
+
+        return self._from_native_series(pc.fill_null(ser, pa.scalar(value, dtype)))
+
     @property
     def shape(self) -> tuple[int]:
         return (len(self._native_series),)
@@ -361,16 +375,50 @@ class ArrowSeriesCatNamespace:
 
 
 class ArrowSeriesStringNamespace:
-    def __init__(self, series: ArrowSeries) -> None:
+    def __init__(self: Self, series: ArrowSeries) -> None:
         self._arrow_series = series
 
-    def to_uppercase(self) -> ArrowSeries:
+    def starts_with(self: Self, prefix: str) -> ArrowSeries:
+        pc = get_pyarrow_compute()
+        return self._arrow_series._from_native_series(
+            pc.equal(self.slice(0, len(prefix))._native_series, prefix)
+        )
+
+    def ends_with(self: Self, suffix: str) -> ArrowSeries:
+        pc = get_pyarrow_compute()
+        return self._arrow_series._from_native_series(
+            pc.equal(self.slice(-len(suffix))._native_series, suffix)
+        )
+
+    def contains(self: Self, pattern: str, *, literal: bool = False) -> ArrowSeries:
+        pc = get_pyarrow_compute()
+        check_func = pc.match_substring if literal else pc.match_substring_regex
+        return self._arrow_series._from_native_series(
+            check_func(self._arrow_series._native_series, pattern)
+        )
+
+    def slice(self: Self, offset: int, length: int | None = None) -> ArrowSeries:
+        pc = get_pyarrow_compute()
+        stop = offset + length if length else None
+        return self._arrow_series._from_native_series(
+            pc.utf8_slice_codeunits(
+                self._arrow_series._native_series, start=offset, stop=stop
+            ),
+        )
+
+    def to_datetime(self: Self, format: str | None = None) -> ArrowSeries:  # noqa: A002
+        pc = get_pyarrow_compute()
+        return self._arrow_series._from_native_series(
+            pc.strptime(self._arrow_series._native_series, format=format, unit="us")
+        )
+
+    def to_uppercase(self: Self) -> ArrowSeries:
         pc = get_pyarrow_compute()
         return self._arrow_series._from_native_series(
             pc.utf8_upper(self._arrow_series._native_series),
         )
 
-    def to_lowercase(self) -> ArrowSeries:
+    def to_lowercase(self: Self) -> ArrowSeries:
         pc = get_pyarrow_compute()
         return self._arrow_series._from_native_series(
             pc.utf8_lower(self._arrow_series._native_series),
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index fa6e707c..6606e8f1 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -13,7 +13,6 @@ from narwhals._expression_parsing import evaluate_into_exprs
 from narwhals._pandas_like.expr import PandasLikeExpr
 from narwhals._pandas_like.utils import Implementation
 from narwhals._pandas_like.utils import create_native_series
-from narwhals._pandas_like.utils import generate_unique_token
 from narwhals._pandas_like.utils import horizontal_concat
 from narwhals._pandas_like.utils import translate_dtype
 from narwhals._pandas_like.utils import validate_dataframe_comparand
@@ -23,6 +22,7 @@ from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
 from narwhals.utils import flatten
+from narwhals.utils import generate_unique_token
 
 if TYPE_CHECKING:
     from typing_extensions import Self
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 3548966c..b493cfed 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import secrets
 from enum import Enum
 from enum import auto
 from typing import TYPE_CHECKING
@@ -458,31 +457,3 @@ def int_dtype_mapper(dtype: Any) -> str:
     if str(dtype).lower() != str(dtype):  # pragma: no cover
         return "Int64"
     return "int64"
-
-
-def generate_unique_token(n_bytes: int, columns: list[str]) -> str:  # pragma: no cover
-    """Generates a unique token of specified n_bytes that is not present in the given list of columns.
-
-    Arguments:
-        n_bytes : The number of bytes to generate for the token.
-        columns : The list of columns to check for uniqueness.
-
-    Returns:
-        A unique token that is not present in the given list of columns.
-
-    Raises:
-        AssertionError: If a unique token cannot be generated after 100 attempts.
-    """
-    counter = 0
-    while True:
-        token = secrets.token_hex(n_bytes)
-        if token not in columns:
-            return token
-
-        counter += 1
-        if counter > 100:
-            msg = (
-                "Internal Error: Narwhals was not able to generate a column name to perform cross "
-                "join operation"
-            )
-            raise AssertionError(msg)
diff --git a/narwhals/_polars/__init__.py b/narwhals/_polars/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
new file mode 100644
index 00000000..70a24c05
--- /dev/null
+++ b/narwhals/_polars/dataframe.py
@@ -0,0 +1,188 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing import Any
+
+from narwhals._polars.namespace import PolarsNamespace
+from narwhals._polars.utils import extract_args_kwargs
+from narwhals._polars.utils import translate_dtype
+from narwhals.dependencies import get_polars
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+
+class PolarsDataFrame:
+    def __init__(self, df: Any, *, backend_version: tuple[int, ...]) -> None:
+        self._native_dataframe = df
+        self._backend_version = backend_version
+
+    def __repr__(self) -> str:  # pragma: no cover
+        return "PolarsDataFrame"
+
+    def __narwhals_dataframe__(self) -> Self:
+        return self
+
+    def __narwhals_namespace__(self) -> PolarsNamespace:
+        return PolarsNamespace(backend_version=self._backend_version)
+
+    def __native_namespace__(self) -> Any:
+        return get_polars()
+
+    def _from_native_dataframe(self, df: Any) -> Self:
+        return self.__class__(df, backend_version=self._backend_version)
+
+    def _from_native_object(self, obj: Any) -> Any:
+        pl = get_polars()
+        if isinstance(obj, pl.Series):
+            from narwhals._polars.series import PolarsSeries
+
+            return PolarsSeries(obj, backend_version=self._backend_version)
+        if isinstance(obj, pl.DataFrame):
+            return self._from_native_dataframe(obj)
+        # scalar
+        return obj
+
+    def __getattr__(self, attr: str) -> Any:
+        if attr == "collect":  # pragma: no cover
+            raise AttributeError
+
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._from_native_object(
+                getattr(self._native_dataframe, attr)(*args, **kwargs)
+            )
+
+        return func
+
+    @property
+    def schema(self) -> dict[str, Any]:
+        schema = self._native_dataframe.schema
+        return {name: translate_dtype(dtype) for name, dtype in schema.items()}
+
+    def collect_schema(self) -> dict[str, Any]:
+        if self._backend_version < (1,):  # pragma: no cover
+            schema = self._native_dataframe.schema
+        else:
+            schema = dict(self._native_dataframe.collect_schema())
+        return {name: translate_dtype(dtype) for name, dtype in schema.items()}
+
+    @property
+    def shape(self) -> tuple[int, int]:
+        return self._native_dataframe.shape  # type: ignore[no-any-return]
+
+    def __getitem__(self, item: Any) -> Any:
+        pl = get_polars()
+        result = self._native_dataframe.__getitem__(item)
+        if isinstance(result, pl.Series):
+            from narwhals._polars.series import PolarsSeries
+
+            return PolarsSeries(result, backend_version=self._backend_version)
+        return self._from_native_object(result)
+
+    def get_column(self, name: str) -> Any:
+        from narwhals._polars.series import PolarsSeries
+
+        return PolarsSeries(
+            self._native_dataframe.get_column(name), backend_version=self._backend_version
+        )
+
+    def is_empty(self) -> bool:
+        return len(self._native_dataframe) == 0
+
+    @property
+    def columns(self) -> list[str]:
+        return self._native_dataframe.columns  # type: ignore[no-any-return]
+
+    def lazy(self) -> PolarsLazyFrame:
+        return PolarsLazyFrame(
+            self._native_dataframe.lazy(), backend_version=self._backend_version
+        )
+
+    def to_dict(self, *, as_series: bool) -> Any:
+        df = self._native_dataframe
+
+        if as_series:
+            from narwhals._polars.series import PolarsSeries
+
+            return {
+                name: PolarsSeries(col, backend_version=self._backend_version)
+                for name, col in df.to_dict(as_series=True).items()
+            }
+        else:
+            return df.to_dict(as_series=False)
+
+    def group_by(self, by: list[str]) -> Any:
+        from narwhals._polars.group_by import PolarsGroupBy
+
+        return PolarsGroupBy(self, by)
+
+    def with_row_index(self, name: str) -> Any:
+        if self._backend_version < (0, 20, 4):  # pragma: no cover
+            return self._from_native_dataframe(
+                self._native_dataframe.with_row_count(name)
+            )
+        return self._from_native_dataframe(self._native_dataframe.with_row_index(name))
+
+
+class PolarsLazyFrame:
+    def __init__(self, df: Any, *, backend_version: tuple[int, ...]) -> None:
+        self._native_dataframe = df
+        self._backend_version = backend_version
+
+    def __repr__(self) -> str:  # pragma: no cover
+        return "PolarsLazyFrame"
+
+    def __narwhals_lazyframe__(self) -> Self:
+        return self
+
+    def __narwhals_namespace__(self) -> PolarsNamespace:
+        return PolarsNamespace(backend_version=self._backend_version)
+
+    def __native_namespace__(self) -> Any:  # pragma: no cover
+        return get_polars()
+
+    def _from_native_dataframe(self, df: Any) -> Self:
+        return self.__class__(df, backend_version=self._backend_version)
+
+    def __getattr__(self, attr: str) -> Any:
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._from_native_dataframe(
+                getattr(self._native_dataframe, attr)(*args, **kwargs)
+            )
+
+        return func
+
+    @property
+    def columns(self) -> list[str]:
+        return self._native_dataframe.columns  # type: ignore[no-any-return]
+
+    @property
+    def schema(self) -> dict[str, Any]:
+        schema = self._native_dataframe.schema
+        return {name: translate_dtype(dtype) for name, dtype in schema.items()}
+
+    def collect_schema(self) -> dict[str, Any]:
+        if self._backend_version < (1,):  # pragma: no cover
+            schema = self._native_dataframe.schema
+        else:
+            schema = dict(self._native_dataframe.collect_schema())
+        return {name: translate_dtype(dtype) for name, dtype in schema.items()}
+
+    def collect(self) -> PolarsDataFrame:
+        return PolarsDataFrame(
+            self._native_dataframe.collect(), backend_version=self._backend_version
+        )
+
+    def group_by(self, by: list[str]) -> Any:
+        from narwhals._polars.group_by import PolarsLazyGroupBy
+
+        return PolarsLazyGroupBy(self, by)
+
+    def with_row_index(self, name: str) -> Any:
+        if self._backend_version < (0, 20, 4):  # pragma: no cover
+            return self._from_native_dataframe(
+                self._native_dataframe.with_row_count(name)
+            )
+        return self._from_native_dataframe(self._native_dataframe.with_row_index(name))
diff --git a/narwhals/_polars/expr.py b/narwhals/_polars/expr.py
new file mode 100644
index 00000000..779bafcf
--- /dev/null
+++ b/narwhals/_polars/expr.py
@@ -0,0 +1,149 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing import Any
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+    from narwhals.dtypes import DType
+from narwhals._polars.namespace import PolarsNamespace
+from narwhals._polars.utils import extract_args_kwargs
+from narwhals._polars.utils import extract_native
+from narwhals._polars.utils import reverse_translate_dtype
+
+
+class PolarsExpr:
+    def __init__(self, expr: Any) -> None:
+        self._native_expr = expr
+
+    def __repr__(self) -> str:  # pragma: no cover
+        return "PolarsExpr"
+
+    def __narwhals_expr__(self) -> Self:  # pragma: no cover
+        return self
+
+    def __narwhals_namespace__(self) -> PolarsNamespace:  # pragma: no cover
+        return PolarsNamespace(backend_version=self._backend_version)
+
+    def _from_native_expr(self, expr: Any) -> Self:
+        return self.__class__(expr)
+
+    def __getattr__(self, attr: str) -> Any:
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._from_native_expr(
+                getattr(self._native_expr, attr)(*args, **kwargs)
+            )
+
+        return func
+
+    def cast(self, dtype: DType) -> Self:
+        expr = self._native_expr
+        dtype = reverse_translate_dtype(dtype)
+        return self._from_native_expr(expr.cast(dtype))
+
+    def __eq__(self, other: object) -> Self:  # type: ignore[override]
+        return self._from_native_expr(self._native_expr.__eq__(extract_native(other)))
+
+    def __ne__(self, other: object) -> Self:  # type: ignore[override]
+        return self._from_native_expr(self._native_expr.__ne__(extract_native(other)))
+
+    def __ge__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__ge__(extract_native(other)))
+
+    def __gt__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__gt__(extract_native(other)))
+
+    def __le__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__le__(extract_native(other)))
+
+    def __lt__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__lt__(extract_native(other)))
+
+    def __and__(self, other: PolarsExpr | bool | Any) -> Self:
+        return self._from_native_expr(self._native_expr.__and__(extract_native(other)))
+
+    def __or__(self, other: PolarsExpr | bool | Any) -> Self:
+        return self._from_native_expr(self._native_expr.__or__(extract_native(other)))
+
+    def __add__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__add__(extract_native(other)))
+
+    def __radd__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__radd__(extract_native(other)))
+
+    def __sub__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__sub__(extract_native(other)))
+
+    def __rsub__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__rsub__(extract_native(other)))
+
+    def __mul__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__mul__(extract_native(other)))
+
+    def __rmul__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__rmul__(extract_native(other)))
+
+    def __pow__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__pow__(extract_native(other)))
+
+    def __rpow__(self, other: Any) -> Self:
+        return self._from_native_expr(self._native_expr.__rpow__(extract_native(other)))
+
+    def __invert__(self) -> Self:
+        return self._from_native_expr(self._native_expr.__invert__())
+
+    @property
+    def dt(self) -> PolarsExprDateTimeNamespace:
+        return PolarsExprDateTimeNamespace(self)
+
+    @property
+    def str(self) -> PolarsExprStringNamespace:
+        return PolarsExprStringNamespace(self)
+
+    @property
+    def cat(self) -> PolarsExprCatNamespace:
+        return PolarsExprCatNamespace(self)
+
+
+class PolarsExprDateTimeNamespace:
+    def __init__(self, expr: PolarsExpr) -> None:
+        self._expr = expr
+
+    def __getattr__(self, attr: str) -> Any:
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._expr._from_native_expr(
+                getattr(self._expr._native_expr.dt, attr)(*args, **kwargs)
+            )
+
+        return func
+
+
+class PolarsExprStringNamespace:
+    def __init__(self, expr: PolarsExpr) -> None:
+        self._expr = expr
+
+    def __getattr__(self, attr: str) -> Any:
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._expr._from_native_expr(
+                getattr(self._expr._native_expr.str, attr)(*args, **kwargs)
+            )
+
+        return func
+
+
+class PolarsExprCatNamespace:
+    def __init__(self, expr: PolarsExpr) -> None:
+        self._expr = expr
+
+    def __getattr__(self, attr: str) -> Any:
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._expr._from_native_expr(
+                getattr(self._expr._native_expr.cat, attr)(*args, **kwargs)
+            )
+
+        return func
diff --git a/narwhals/_polars/group_by.py b/narwhals/_polars/group_by.py
new file mode 100644
index 00000000..c0de7573
--- /dev/null
+++ b/narwhals/_polars/group_by.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing import Any
+
+from narwhals._polars.utils import extract_args_kwargs
+
+if TYPE_CHECKING:
+    from narwhals._polars.dataframe import PolarsDataFrame
+    from narwhals._polars.dataframe import PolarsLazyFrame
+
+
+class PolarsGroupBy:
+    def __init__(self, df: Any, keys: list[str]) -> None:
+        self._compliant_frame = df
+        self.keys = keys
+        self._grouped = df._native_dataframe.group_by(keys)
+
+    def agg(self, *aggs: Any, **named_aggs: Any) -> PolarsDataFrame:
+        aggs, named_aggs = extract_args_kwargs(aggs, named_aggs)  # type: ignore[assignment]
+        return self._compliant_frame._from_native_dataframe(  # type: ignore[no-any-return]
+            self._grouped.agg(*aggs, **named_aggs),
+        )
+
+    def __iter__(self) -> Any:
+        for key, df in self._grouped:
+            yield tuple(key), self._compliant_frame._from_native_dataframe(df)
+
+
+class PolarsLazyGroupBy:
+    def __init__(self, df: Any, keys: list[str]) -> None:
+        self._compliant_frame = df
+        self.keys = keys
+        self._grouped = df._native_dataframe.group_by(keys)
+
+    def agg(self, *aggs: Any, **named_aggs: Any) -> PolarsLazyFrame:
+        aggs, named_aggs = extract_args_kwargs(aggs, named_aggs)  # type: ignore[assignment]
+        return self._compliant_frame._from_native_dataframe(  # type: ignore[no-any-return]
+            self._grouped.agg(*aggs, **named_aggs),
+        )
diff --git a/narwhals/_polars/namespace.py b/narwhals/_polars/namespace.py
new file mode 100644
index 00000000..ed86c07e
--- /dev/null
+++ b/narwhals/_polars/namespace.py
@@ -0,0 +1,128 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing import Any
+from typing import Iterable
+from typing import Sequence
+
+from narwhals import dtypes
+from narwhals._polars.utils import extract_args_kwargs
+from narwhals._polars.utils import reverse_translate_dtype
+from narwhals.dependencies import get_polars
+
+if TYPE_CHECKING:
+    from narwhals._polars.dataframe import PolarsDataFrame
+    from narwhals._polars.dataframe import PolarsLazyFrame
+    from narwhals._polars.expr import PolarsExpr
+
+
+class PolarsNamespace:
+    Int64 = dtypes.Int64
+    Int32 = dtypes.Int32
+    Int16 = dtypes.Int16
+    Int8 = dtypes.Int8
+    UInt64 = dtypes.UInt64
+    UInt32 = dtypes.UInt32
+    UInt16 = dtypes.UInt16
+    UInt8 = dtypes.UInt8
+    Float64 = dtypes.Float64
+    Float32 = dtypes.Float32
+    Boolean = dtypes.Boolean
+    Object = dtypes.Object
+    Unknown = dtypes.Unknown
+    Categorical = dtypes.Categorical
+    Enum = dtypes.Enum
+    String = dtypes.String
+    Datetime = dtypes.Datetime
+    Duration = dtypes.Duration
+    Date = dtypes.Date
+
+    def __init__(self, *, backend_version: tuple[int, ...]) -> None:
+        self._backend_version = backend_version
+
+    def __getattr__(self, attr: str) -> Any:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return PolarsExpr(getattr(pl, attr)(*args, **kwargs))
+
+        return func
+
+    def len(self) -> PolarsExpr:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+        if self._backend_version < (0, 20, 4):  # pragma: no cover
+            return PolarsExpr(pl.count().alias("len"))
+        return PolarsExpr(pl.len())
+
+    def concat(
+        self,
+        items: Sequence[PolarsDataFrame | PolarsLazyFrame],
+        *,
+        how: str = "vertical",
+    ) -> PolarsDataFrame | PolarsLazyFrame:
+        from narwhals._polars.dataframe import PolarsDataFrame
+        from narwhals._polars.dataframe import PolarsLazyFrame
+
+        pl = get_polars()
+        dfs: list[Any] = [item._native_dataframe for item in items]
+        result = pl.concat(dfs, how=how)
+        if isinstance(result, pl.DataFrame):
+            return PolarsDataFrame(result, backend_version=items[0]._backend_version)
+        return PolarsLazyFrame(result, backend_version=items[0]._backend_version)
+
+    def lit(self, value: Any, dtype: dtypes.DType | None = None) -> PolarsExpr:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+        if dtype is not None:
+            return PolarsExpr(pl.lit(value, dtype=reverse_translate_dtype(dtype)))
+        return PolarsExpr(pl.lit(value))
+
+    @property
+    def selectors(self) -> PolarsSelectors:
+        return PolarsSelectors()
+
+
+class PolarsSelectors:
+    def by_dtype(self, dtypes: Iterable[dtypes.DType]) -> PolarsExpr:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+        return PolarsExpr(
+            pl.selectors.by_dtype([reverse_translate_dtype(dtype) for dtype in dtypes])
+        )
+
+    def numeric(self) -> PolarsExpr:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+        return PolarsExpr(pl.selectors.numeric())
+
+    def boolean(self) -> PolarsExpr:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+        return PolarsExpr(pl.selectors.boolean())
+
+    def string(self) -> PolarsExpr:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+        return PolarsExpr(pl.selectors.string())
+
+    def categorical(self) -> PolarsExpr:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+        return PolarsExpr(pl.selectors.categorical())
+
+    def all(self) -> PolarsExpr:
+        from narwhals._polars.expr import PolarsExpr
+
+        pl = get_polars()
+        return PolarsExpr(pl.selectors.all())
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
new file mode 100644
index 00000000..372d83ad
--- /dev/null
+++ b/narwhals/_polars/series.py
@@ -0,0 +1,215 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from typing import Any
+
+from narwhals._polars.utils import extract_args_kwargs
+from narwhals._polars.utils import extract_native
+from narwhals.dependencies import get_polars
+
+if TYPE_CHECKING:
+    import numpy as np
+    from typing_extensions import Self
+
+    from narwhals.dtypes import DType
+
+from narwhals._polars.namespace import PolarsNamespace
+from narwhals._polars.utils import reverse_translate_dtype
+from narwhals._polars.utils import translate_dtype
+
+PL = get_polars()
+
+
+class PolarsSeries:
+    def __init__(self, series: Any, *, backend_version: tuple[int, ...]) -> None:
+        self._native_series = series
+        self._backend_version = backend_version
+
+    def __repr__(self) -> str:  # pragma: no cover
+        return "PolarsSeries"
+
+    def __narwhals_series__(self) -> Self:
+        return self
+
+    def __native_namespace__(self) -> Any:
+        return get_polars()
+
+    def __narwhals_namespace__(self) -> PolarsNamespace:
+        return PolarsNamespace(backend_version=self._backend_version)
+
+    def _from_native_series(self, series: Any) -> Self:
+        return self.__class__(series, backend_version=self._backend_version)
+
+    def _from_native_object(self, series: Any) -> Any:
+        pl = get_polars()
+        if isinstance(series, pl.Series):
+            return self._from_native_series(series)
+        if isinstance(series, pl.DataFrame):
+            from narwhals._polars.dataframe import PolarsDataFrame
+
+            return PolarsDataFrame(series, backend_version=self._backend_version)
+        # scalar
+        return series
+
+    def __getattr__(self, attr: str) -> Any:
+        if attr == "as_py":  # pragma: no cover
+            raise AttributeError
+
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._from_native_object(
+                getattr(self._native_series, attr)(*args, **kwargs)
+            )
+
+        return func
+
+    def __len__(self) -> int:
+        return len(self._native_series)
+
+    @property
+    def shape(self) -> tuple[int]:
+        return (len(self),)
+
+    @property
+    def name(self) -> str:
+        return self._native_series.name  # type: ignore[no-any-return]
+
+    @property
+    def dtype(self) -> DType:
+        return translate_dtype(self._native_series.dtype)
+
+    def __getitem__(self, item: Any) -> Any:
+        return self._from_native_object(self._native_series.__getitem__(item))
+
+    def cast(self, dtype: DType) -> Self:
+        ser = self._native_series
+        dtype = reverse_translate_dtype(dtype)
+        return self._from_native_series(ser.cast(dtype))
+
+    def __array__(self, dtype: Any = None, copy: bool | None = None) -> np.ndarray:
+        if self._backend_version < (0, 20, 29):  # pragma: no cover
+            return self._native_series.__array__(dtype=dtype)
+        return self._native_series.__array__(dtype=dtype, copy=copy)
+
+    def __eq__(self, other: object) -> Self:  # type: ignore[override]
+        return self._from_native_series(self._native_series.__eq__(extract_native(other)))
+
+    def __ne__(self, other: object) -> Self:  # type: ignore[override]
+        return self._from_native_series(self._native_series.__ne__(extract_native(other)))
+
+    def __ge__(self, other: Any) -> Self:
+        return self._from_native_series(self._native_series.__ge__(extract_native(other)))
+
+    def __gt__(self, other: Any) -> Self:
+        return self._from_native_series(self._native_series.__gt__(extract_native(other)))
+
+    def __le__(self, other: Any) -> Self:
+        return self._from_native_series(self._native_series.__le__(extract_native(other)))
+
+    def __lt__(self, other: Any) -> Self:
+        return self._from_native_series(self._native_series.__lt__(extract_native(other)))
+
+    def __and__(self, other: PolarsSeries | bool | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__and__(extract_native(other))
+        )
+
+    def __or__(self, other: PolarsSeries | bool | Any) -> Self:
+        return self._from_native_series(self._native_series.__or__(extract_native(other)))
+
+    def __add__(self, other: PolarsSeries | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__add__(extract_native(other))
+        )
+
+    def __radd__(self, other: PolarsSeries | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__radd__(extract_native(other))
+        )
+
+    def __sub__(self, other: PolarsSeries | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__sub__(extract_native(other))
+        )
+
+    def __rsub__(self, other: PolarsSeries | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__rsub__(extract_native(other))
+        )
+
+    def __mul__(self, other: PolarsSeries | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__mul__(extract_native(other))
+        )
+
+    def __rmul__(self, other: PolarsSeries | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__rmul__(extract_native(other))
+        )
+
+    def __pow__(self, other: PolarsSeries | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__pow__(extract_native(other))
+        )
+
+    def __rpow__(self, other: PolarsSeries | Any) -> Self:
+        return self._from_native_series(
+            self._native_series.__rpow__(extract_native(other))
+        )
+
+    def __invert__(self) -> Self:
+        return self._from_native_series(self._native_series.__invert__())
+
+    @property
+    def dt(self) -> PolarsSeriesDateTimeNamespace:
+        return PolarsSeriesDateTimeNamespace(self)
+
+    @property
+    def str(self) -> PolarsSeriesStringNamespace:
+        return PolarsSeriesStringNamespace(self)
+
+    @property
+    def cat(self) -> PolarsSeriesCatNamespace:
+        return PolarsSeriesCatNamespace(self)
+
+
+class PolarsSeriesDateTimeNamespace:
+    def __init__(self, series: PolarsSeries) -> None:
+        self._series = series
+
+    def __getattr__(self, attr: str) -> Any:
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._series._from_native_series(
+                getattr(self._series._native_series.dt, attr)(*args, **kwargs)
+            )
+
+        return func
+
+
+class PolarsSeriesStringNamespace:
+    def __init__(self, series: PolarsSeries) -> None:
+        self._series = series
+
+    def __getattr__(self, attr: str) -> Any:
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._series._from_native_series(
+                getattr(self._series._native_series.str, attr)(*args, **kwargs)
+            )
+
+        return func
+
+
+class PolarsSeriesCatNamespace:
+    def __init__(self, series: PolarsSeries) -> None:
+        self._series = series
+
+    def __getattr__(self, attr: str) -> Any:
+        def func(*args: Any, **kwargs: Any) -> Any:
+            args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
+            return self._series._from_native_series(
+                getattr(self._series._native_series.cat, attr)(*args, **kwargs)
+            )
+
+        return func
diff --git a/narwhals/_polars/utils.py b/narwhals/_polars/utils.py
new file mode 100644
index 00000000..2dc92b3a
--- /dev/null
+++ b/narwhals/_polars/utils.py
@@ -0,0 +1,111 @@
+from __future__ import annotations
+
+from typing import Any
+
+from narwhals import dtypes
+from narwhals.dependencies import get_polars
+
+
+def extract_native(obj: Any) -> Any:
+    from narwhals._polars.dataframe import PolarsDataFrame
+    from narwhals._polars.dataframe import PolarsLazyFrame
+    from narwhals._polars.expr import PolarsExpr
+    from narwhals._polars.series import PolarsSeries
+
+    if isinstance(obj, (PolarsDataFrame, PolarsLazyFrame)):
+        return obj._native_dataframe
+    if isinstance(obj, PolarsSeries):
+        return obj._native_series
+    if isinstance(obj, PolarsExpr):
+        return obj._native_expr
+    return obj
+
+
+def extract_args_kwargs(args: Any, kwargs: Any) -> tuple[list[Any], dict[str, Any]]:
+    args = [extract_native(arg) for arg in args]
+    kwargs = {k: extract_native(v) for k, v in kwargs.items()}
+    return args, kwargs
+
+
+def translate_dtype(dtype: Any) -> dtypes.DType:
+    pl = get_polars()
+    if dtype == pl.Float64:
+        return dtypes.Float64()
+    if dtype == pl.Float32:
+        return dtypes.Float32()
+    if dtype == pl.Int64:
+        return dtypes.Int64()
+    if dtype == pl.Int32:
+        return dtypes.Int32()
+    if dtype == pl.Int16:
+        return dtypes.Int16()
+    if dtype == pl.Int8:
+        return dtypes.Int8()
+    if dtype == pl.UInt64:
+        return dtypes.UInt64()
+    if dtype == pl.UInt32:
+        return dtypes.UInt32()
+    if dtype == pl.UInt16:
+        return dtypes.UInt16()
+    if dtype == pl.UInt8:
+        return dtypes.UInt8()
+    if dtype == pl.String:
+        return dtypes.String()
+    if dtype == pl.Boolean:
+        return dtypes.Boolean()
+    if dtype == pl.Object:
+        return dtypes.Object()
+    if dtype == pl.Categorical:
+        return dtypes.Categorical()
+    if dtype == pl.Enum:
+        return dtypes.Enum()
+    if dtype == pl.Datetime:
+        return dtypes.Datetime()
+    if dtype == pl.Duration:
+        return dtypes.Duration()
+    if dtype == pl.Date:
+        return dtypes.Date()
+    return dtypes.Unknown()
+
+
+def reverse_translate_dtype(dtype: dtypes.DType | type[dtypes.DType]) -> Any:
+    pl = get_polars()
+    from narwhals import dtypes
+
+    if dtype == dtypes.Float64:
+        return pl.Float64()
+    if dtype == dtypes.Float32:
+        return pl.Float32()
+    if dtype == dtypes.Int64:
+        return pl.Int64()
+    if dtype == dtypes.Int32:
+        return pl.Int32()
+    if dtype == dtypes.Int16:
+        return pl.Int16()
+    if dtype == dtypes.Int8:
+        return pl.Int8()
+    if dtype == dtypes.UInt64:
+        return pl.UInt64()
+    if dtype == dtypes.UInt32:
+        return pl.UInt32()
+    if dtype == dtypes.UInt16:
+        return pl.UInt16()
+    if dtype == dtypes.UInt8:
+        return pl.UInt8()
+    if dtype == dtypes.String:
+        return pl.String()
+    if dtype == dtypes.Boolean:
+        return pl.Boolean()
+    if dtype == dtypes.Object:  # pragma: no cover
+        return pl.Object()
+    if dtype == dtypes.Categorical:
+        return pl.Categorical()
+    if dtype == dtypes.Enum:  # pragma: no cover
+        return pl.Enum()
+    if dtype == dtypes.Datetime:
+        return pl.Datetime()
+    if dtype == dtypes.Duration:
+        return pl.Duration()
+    if dtype == dtypes.Date:
+        return pl.Date()
+    return pl.Unknown()  # pragma: no cover
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 85130aff..fb690f49 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -13,10 +13,8 @@ from typing import overload
 
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_polars
-from narwhals.dtypes import to_narwhals_dtype
 from narwhals.schema import Schema
 from narwhals.utils import flatten
-from narwhals.utils import validate_same_library
 
 if TYPE_CHECKING:
     from io import BytesIO
@@ -36,29 +34,21 @@ FrameT = TypeVar("FrameT", bound="IntoDataFrame")
 
 class BaseFrame(Generic[FrameT]):
     _compliant_frame: Any
-    _is_polars: bool
-    _backend_version: tuple[int, ...]
     _level: Literal["full", "interchange"]
 
     def __len__(self) -> Any:
         return self._compliant_frame.__len__()
 
     def __native_namespace__(self) -> Any:
-        if self._is_polars:
-            return get_polars()
         return self._compliant_frame.__native_namespace__()
 
     def __narwhals_namespace__(self) -> Any:
-        if self._is_polars:
-            return get_polars()
         return self._compliant_frame.__narwhals_namespace__()
 
     def _from_compliant_dataframe(self, df: Any) -> Self:
         # construct, preserving properties
         return self.__class__(  # type: ignore[call-arg]
             df,
-            is_polars=self._is_polars,
-            backend_version=self._backend_version,
             level=self._level,
         )
 
@@ -90,34 +80,17 @@ class BaseFrame(Generic[FrameT]):
 
     @property
     def schema(self) -> Schema:
-        return Schema(
-            {
-                k: to_narwhals_dtype(v, is_polars=self._is_polars)
-                for k, v in self._compliant_frame.schema.items()
-            }
-        )
+        return Schema(self._compliant_frame.schema.items())
 
     def collect_schema(self) -> Schema:
-        if self._is_polars and self._backend_version < (1, 0, 0):  # pragma: no cover
-            native_schema = self._compliant_frame.schema
-        else:
-            native_schema = dict(self._compliant_frame.collect_schema())
+        native_schema = dict(self._compliant_frame.collect_schema())
 
-        return Schema(
-            {
-                k: to_narwhals_dtype(v, is_polars=self._is_polars)
-                for k, v in native_schema.items()
-            }
-        )
+        return Schema(native_schema)
 
     def pipe(self, function: Callable[[Any], Self], *args: Any, **kwargs: Any) -> Self:
         return function(self, *args, **kwargs)
 
     def with_row_index(self, name: str = "index") -> Self:
-        if self._is_polars and self._backend_version < (0, 20, 4):  # pragma: no cover
-            return self._from_compliant_dataframe(
-                self._compliant_frame.with_row_count(name),
-            )
         return self._from_compliant_dataframe(
             self._compliant_frame.with_row_index(name),
         )
@@ -134,8 +107,6 @@ class BaseFrame(Generic[FrameT]):
     def lazy(self) -> LazyFrame[Any]:
         return LazyFrame(
             self._compliant_frame.lazy(),
-            is_polars=self._is_polars,
-            backend_version=self._backend_version,
             level=self._level,
         )
 
@@ -206,7 +177,6 @@ class BaseFrame(Generic[FrameT]):
             msg = "Can not pass left_on, right_on for cross join"
             raise ValueError(msg)
 
-        validate_same_library([self, other])
         return self._from_compliant_dataframe(
             self._compliant_frame.join(
                 self._extract_compliant(other),
@@ -234,20 +204,14 @@ class DataFrame(BaseFrame[FrameT]):
         self,
         df: Any,
         *,
-        backend_version: tuple[int, ...],
-        is_polars: bool,
         level: Literal["full", "interchange"],
     ) -> None:
-        self._is_polars = is_polars
-        self._backend_version = backend_version
         self._level: Literal["full", "interchange"] = level
         if hasattr(df, "__narwhals_dataframe__"):
             self._compliant_frame: Any = df.__narwhals_dataframe__()
-        elif is_polars and isinstance(df, get_polars().DataFrame):
-            self._compliant_frame = df
-        else:
-            msg = f"Expected Polars DataFrame or an object which implements `__narwhals_dataframe__`, got: {type(df)}"
-            raise TypeError(msg)
+        else:  # pragma: no cover
+            msg = f"Expected an object which implements `__narwhals_dataframe__`, got: {type(df)}"
+            raise AssertionError(msg)
 
     def __array__(self) -> np.ndarray:
         return self._compliant_frame.to_numpy()
@@ -471,8 +435,6 @@ class DataFrame(BaseFrame[FrameT]):
 
         return Series(
             self._compliant_frame.get_column(name),
-            backend_version=self._backend_version,
-            is_polars=self._is_polars,
             level=self._level,
         )
 
@@ -541,8 +503,6 @@ class DataFrame(BaseFrame[FrameT]):
 
             return Series(
                 self._compliant_frame[item],
-                backend_version=self._backend_version,
-                is_polars=self._is_polars,
                 level=self._level,
             )
 
@@ -607,8 +567,6 @@ class DataFrame(BaseFrame[FrameT]):
             return {
                 key: Series(
                     value,
-                    backend_version=self._backend_version,
-                    is_polars=self._is_polars,
                     level=self._level,
                 )
                 for key, value in self._compliant_frame.to_dict(
@@ -1754,8 +1712,6 @@ class DataFrame(BaseFrame[FrameT]):
 
         return Series(
             self._compliant_frame.is_duplicated(),
-            backend_version=self._backend_version,
-            is_polars=self._is_polars,
             level=self._level,
         )
 
@@ -1841,8 +1797,6 @@ class DataFrame(BaseFrame[FrameT]):
 
         return Series(
             self._compliant_frame.is_unique(),
-            backend_version=self._backend_version,
-            is_polars=self._is_polars,
             level=self._level,
         )
 
@@ -1983,22 +1937,14 @@ class LazyFrame(BaseFrame[FrameT]):
         self,
         df: Any,
         *,
-        is_polars: bool,
-        backend_version: tuple[int, ...],
         level: Literal["full", "interchange"],
     ) -> None:
-        self._is_polars = is_polars
-        self._backend_version = backend_version
         self._level = level
         if hasattr(df, "__narwhals_lazyframe__"):
             self._compliant_frame: Any = df.__narwhals_lazyframe__()
-        elif is_polars and (
-            (pl := get_polars()) is not None and isinstance(df, pl.LazyFrame)
-        ):
-            self._compliant_frame = df
-        else:
+        else:  # pragma: no cover
             msg = f"Expected Polars LazyFrame or an object that implements `__narwhals_lazyframe__`, got: {type(df)}"
-            raise TypeError(msg)
+            raise AssertionError(msg)
 
     def __repr__(self) -> str:  # pragma: no cover
         header = " Narwhals LazyFrame                            "
@@ -2056,8 +2002,6 @@ class LazyFrame(BaseFrame[FrameT]):
         """
         return DataFrame(
             self._compliant_frame.collect(),
-            is_polars=self._is_polars,
-            backend_version=self._backend_version,
             level=self._level,
         )
 
diff --git a/narwhals/dtypes.py b/narwhals/dtypes.py
index 66fffdf3..9b56d614 100644
--- a/narwhals/dtypes.py
+++ b/narwhals/dtypes.py
@@ -3,7 +3,6 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 
-from narwhals.dependencies import get_polars
 from narwhals.utils import isinstance_or_issubclass
 
 if TYPE_CHECKING:
@@ -134,47 +133,3 @@ def translate_dtype(plx: Any, dtype: DType) -> Any:
         return plx.Date
     msg = f"Unknown dtype: {dtype}"  # pragma: no cover
     raise AssertionError(msg)
-
-
-def to_narwhals_dtype(dtype: Any, *, is_polars: bool) -> DType:
-    if not is_polars:
-        return dtype  # type: ignore[no-any-return]
-    pl = get_polars()
-
-    if dtype == pl.Float64:
-        return Float64()
-    if dtype == pl.Float32:
-        return Float32()
-    if dtype == pl.Int64:
-        return Int64()
-    if dtype == pl.Int32:
-        return Int32()
-    if dtype == pl.Int16:
-        return Int16()
-    if dtype == pl.Int8:
-        return Int8()
-    if dtype == pl.UInt64:
-        return UInt64()
-    if dtype == pl.UInt32:
-        return UInt32()
-    if dtype == pl.UInt16:
-        return UInt16()
-    if dtype == pl.UInt8:
-        return UInt8()
-    if dtype == pl.String:
-        return String()
-    if dtype == pl.Boolean:
-        return Boolean()
-    if dtype == pl.Object:
-        return Object()
-    if dtype == pl.Categorical:
-        return Categorical()
-    if dtype == pl.Enum:
-        return Enum()
-    if dtype == pl.Datetime:
-        return Datetime()
-    if dtype == pl.Duration:
-        return Duration()
-    if dtype == pl.Date:
-        return Date()
-    return Unknown()
diff --git a/narwhals/expression.py b/narwhals/expression.py
index a6af772a..6aded757 100644
--- a/narwhals/expression.py
+++ b/narwhals/expression.py
@@ -7,11 +7,9 @@ from typing import Iterable
 from typing import Literal
 
 from narwhals.dependencies import get_numpy
-from narwhals.dependencies import get_polars
 from narwhals.dtypes import DType
 from narwhals.dtypes import translate_dtype
 from narwhals.utils import flatten
-from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -19,7 +17,7 @@ if TYPE_CHECKING:
     from narwhals.typing import IntoExpr
 
 
-def extract_native(expr: Expr, other: Any) -> Any:
+def extract_compliant(expr: Expr, other: Any) -> Any:
     from narwhals.series import Series
 
     if isinstance(other, Expr):
@@ -132,122 +130,122 @@ class Expr:
     # --- binary ---
     def __eq__(self, other: object) -> Self:  # type: ignore[override]
         return self.__class__(
-            lambda plx: self._call(plx).__eq__(extract_native(plx, other))
+            lambda plx: self._call(plx).__eq__(extract_compliant(plx, other))
         )
 
     def __ne__(self, other: object) -> Self:  # type: ignore[override]
         return self.__class__(
-            lambda plx: self._call(plx).__ne__(extract_native(plx, other))
+            lambda plx: self._call(plx).__ne__(extract_compliant(plx, other))
         )
 
     def __and__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__and__(extract_native(plx, other))
+            lambda plx: self._call(plx).__and__(extract_compliant(plx, other))
         )
 
     def __rand__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__rand__(extract_native(plx, other))
+            lambda plx: self._call(plx).__rand__(extract_compliant(plx, other))
         )
 
     def __or__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__or__(extract_native(plx, other))
+            lambda plx: self._call(plx).__or__(extract_compliant(plx, other))
         )
 
     def __ror__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__ror__(extract_native(plx, other))
+            lambda plx: self._call(plx).__ror__(extract_compliant(plx, other))
         )
 
     def __add__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__add__(extract_native(plx, other))
+            lambda plx: self._call(plx).__add__(extract_compliant(plx, other))
         )
 
     def __radd__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__radd__(extract_native(plx, other))
+            lambda plx: self._call(plx).__radd__(extract_compliant(plx, other))
         )
 
     def __sub__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__sub__(extract_native(plx, other))
+            lambda plx: self._call(plx).__sub__(extract_compliant(plx, other))
         )
 
     def __rsub__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__rsub__(extract_native(plx, other))
+            lambda plx: self._call(plx).__rsub__(extract_compliant(plx, other))
         )
 
     def __truediv__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__truediv__(extract_native(plx, other))
+            lambda plx: self._call(plx).__truediv__(extract_compliant(plx, other))
         )
 
     def __rtruediv__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__rtruediv__(extract_native(plx, other))
+            lambda plx: self._call(plx).__rtruediv__(extract_compliant(plx, other))
         )
 
     def __mul__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__mul__(extract_native(plx, other))
+            lambda plx: self._call(plx).__mul__(extract_compliant(plx, other))
         )
 
     def __rmul__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__rmul__(extract_native(plx, other))
+            lambda plx: self._call(plx).__rmul__(extract_compliant(plx, other))
         )
 
     def __le__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__le__(extract_native(plx, other))
+            lambda plx: self._call(plx).__le__(extract_compliant(plx, other))
         )
 
     def __lt__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__lt__(extract_native(plx, other))
+            lambda plx: self._call(plx).__lt__(extract_compliant(plx, other))
         )
 
     def __gt__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__gt__(extract_native(plx, other))
+            lambda plx: self._call(plx).__gt__(extract_compliant(plx, other))
         )
 
     def __ge__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__ge__(extract_native(plx, other))
+            lambda plx: self._call(plx).__ge__(extract_compliant(plx, other))
         )
 
     def __pow__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__pow__(extract_native(plx, other))
+            lambda plx: self._call(plx).__pow__(extract_compliant(plx, other))
         )
 
     def __rpow__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__rpow__(extract_native(plx, other))
+            lambda plx: self._call(plx).__rpow__(extract_compliant(plx, other))
         )
 
     def __floordiv__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__floordiv__(extract_native(plx, other))
+            lambda plx: self._call(plx).__floordiv__(extract_compliant(plx, other))
         )
 
     def __rfloordiv__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__rfloordiv__(extract_native(plx, other))
+            lambda plx: self._call(plx).__rfloordiv__(extract_compliant(plx, other))
         )
 
     def __mod__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__mod__(extract_native(plx, other))
+            lambda plx: self._call(plx).__mod__(extract_compliant(plx, other))
         )
 
     def __rmod__(self, other: Any) -> Self:
         return self.__class__(
-            lambda plx: self._call(plx).__rmod__(extract_native(plx, other))
+            lambda plx: self._call(plx).__rmod__(extract_compliant(plx, other))
         )
 
     # --- unary ---
@@ -995,7 +993,7 @@ class Expr:
         """
         return self.__class__(
             lambda plx: self._call(plx).filter(
-                *[extract_native(plx, pred) for pred in flatten(predicates)]
+                *[extract_compliant(plx, pred) for pred in flatten(predicates)]
             )
         )
 
@@ -3051,7 +3049,11 @@ def col(*names: str | Iterable[str]) -> Expr:
         │ 8   │
         └─────┘
     """
-    return Expr(lambda plx: plx.col(*names))
+
+    def func(plx: Any) -> Any:
+        return plx.col(*names)
+
+    return Expr(func)
 
 
 def all() -> Expr:
@@ -3127,12 +3129,6 @@ def len() -> Expr:
     """
 
     def func(plx: Any) -> Any:
-        if (
-            (pl := get_polars()) is not None
-            and plx is pl
-            and parse_version(pl.__version__) <= (0, 20, 4)
-        ):  # pragma: no cover
-            return plx.count().alias("len")
         return plx.len()
 
     return Expr(func)
@@ -3345,7 +3341,9 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         └─────┘
     """
     return Expr(
-        lambda plx: plx.sum_horizontal([extract_native(plx, v) for v in flatten(exprs)])
+        lambda plx: plx.sum_horizontal(
+            *[extract_compliant(plx, v) for v in flatten(exprs)]
+        )
     )
 
 
@@ -3403,7 +3401,9 @@ def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
         └───────┴───────┴───────┘
     """
     return Expr(
-        lambda plx: plx.all_horizontal([extract_native(plx, v) for v in flatten(exprs)])
+        lambda plx: plx.all_horizontal(
+            *[extract_compliant(plx, v) for v in flatten(exprs)]
+        )
     )
 
 
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 24d299be..da6ebdf6 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -12,7 +12,6 @@ from typing import Union
 from narwhals.dataframe import DataFrame
 from narwhals.dataframe import LazyFrame
 from narwhals.utils import validate_laziness
-from narwhals.utils import validate_same_library
 
 # Missing type parameters for generic type "DataFrame"
 # However, trying to provide one results in mypy still complaining...
@@ -35,7 +34,6 @@ def concat(
         msg = "No items to concatenate"
         raise ValueError(msg)
     items = list(items)
-    validate_same_library(items)
     validate_laziness(items)
     first_item = items[0]
     plx = first_item.__narwhals_namespace__()
diff --git a/narwhals/series.py b/narwhals/series.py
index 41664540..027c4e83 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -4,8 +4,6 @@ from typing import TYPE_CHECKING
 from typing import Any
 from typing import Literal
 
-from narwhals.dependencies import get_polars
-from narwhals.dtypes import to_narwhals_dtype
 from narwhals.dtypes import translate_dtype
 
 if TYPE_CHECKING:
@@ -30,26 +28,16 @@ class Series:
         self,
         series: Any,
         *,
-        backend_version: tuple[int, ...],
-        is_polars: bool,
         level: Literal["full", "interchange"],
     ) -> None:
-        self._is_polars = is_polars
-        self._backend_version = backend_version
         self._level = level
         if hasattr(series, "__narwhals_series__"):
             self._compliant_series = series.__narwhals_series__()
-        elif is_polars and (
-            (pl := get_polars()) is not None and isinstance(series, pl.Series)
-        ):
-            self._compliant_series = series
-        else:
+        else:  # pragma: no cover
             msg = f"Expected Polars Series or an object which implements `__narwhals_series__`, got: {type(series)}."
-            raise TypeError(msg)
+            raise AssertionError(msg)
 
     def __array__(self, dtype: Any = None, copy: bool | None = None) -> np.ndarray:
-        if self._is_polars and self._backend_version < (0, 20, 29):  # pragma: no cover
-            return self._compliant_series.__array__(dtype=dtype)
         return self._compliant_series.__array__(dtype=dtype, copy=copy)
 
     def __getitem__(self, idx: int | slice) -> Any:
@@ -58,13 +46,9 @@ class Series:
         return self._from_compliant_series(self._compliant_series[idx])
 
     def __native_namespace__(self) -> Any:
-        if self._is_polars:
-            return get_polars()
         return self._compliant_series.__native_namespace__()
 
     def __narwhals_namespace__(self) -> Any:
-        if self._is_polars:
-            return get_polars()
         return self._compliant_series.__narwhals_namespace__()
 
     @property
@@ -105,8 +89,6 @@ class Series:
     def _from_compliant_series(self, series: Any) -> Self:
         return self.__class__(
             series,
-            is_polars=self._is_polars,
-            backend_version=self._backend_version,
             level=self._level,
         )
 
@@ -182,7 +164,7 @@ class Series:
             >>> func(s_pl)
             Int64
         """
-        return to_narwhals_dtype(self._compliant_series.dtype, is_polars=self._is_polars)
+        return self._compliant_series.dtype
 
     @property
     def name(self) -> str:
@@ -299,8 +281,6 @@ class Series:
 
         return DataFrame(
             self._compliant_series.to_frame(),
-            is_polars=self._is_polars,
-            backend_version=self._backend_version,
             level=self._level,
         )
 
@@ -1678,8 +1658,6 @@ class Series:
 
         return DataFrame(
             self._compliant_series.value_counts(sort=sort, parallel=parallel),
-            is_polars=self._is_polars,
-            backend_version=self._backend_version,
             level=self._level,
         )
 
diff --git a/narwhals/stable/v1.py b/narwhals/stable/v1.py
index 45468d78..959e7d95 100644
--- a/narwhals/stable/v1.py
+++ b/narwhals/stable/v1.py
@@ -466,22 +466,16 @@ def _stableify(
     if isinstance(obj, NwDataFrame):
         return DataFrame(
             obj._compliant_frame,
-            is_polars=obj._is_polars,
-            backend_version=obj._backend_version,
             level=obj._level,
         )
     if isinstance(obj, NwLazyFrame):
         return LazyFrame(
             obj._compliant_frame,
-            is_polars=obj._is_polars,
-            backend_version=obj._backend_version,
             level=obj._level,
         )
     if isinstance(obj, NwSeries):
         return Series(
             obj._compliant_series,
-            is_polars=obj._is_polars,
-            backend_version=obj._backend_version,
             level=obj._level,
         )
     if isinstance(obj, NwExpr):
diff --git a/narwhals/translate.py b/narwhals/translate.py
index e13fb9b3..c0389332 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -57,17 +57,9 @@ def to_native(
     from narwhals.series import Series
 
     if isinstance(narwhals_object, BaseFrame):
-        return (
-            narwhals_object._compliant_frame
-            if narwhals_object._is_polars
-            else narwhals_object._compliant_frame._native_dataframe
-        )
+        return narwhals_object._compliant_frame._native_dataframe
     if isinstance(narwhals_object, Series):
-        return (
-            narwhals_object._compliant_series
-            if narwhals_object._is_polars
-            else narwhals_object._compliant_series._native_series
-        )
+        return narwhals_object._compliant_series._native_series
 
     if strict:
         msg = f"Expected Narwhals object, got {type(narwhals_object)}."
@@ -292,6 +284,9 @@ def from_native(  # noqa: PLR0915
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
     from narwhals._pandas_like.series import PandasLikeSeries
     from narwhals._pandas_like.utils import Implementation
+    from narwhals._polars.dataframe import PolarsDataFrame
+    from narwhals._polars.dataframe import PolarsLazyFrame
+    from narwhals._polars.series import PolarsSeries
     from narwhals.dataframe import DataFrame
     from narwhals.dataframe import LazyFrame
     from narwhals.series import Series
@@ -317,9 +312,7 @@ def from_native(  # noqa: PLR0915
             msg = "Cannot only use `series_only` with polars.DataFrame"
             raise TypeError(msg)
         return DataFrame(
-            native_object,
-            is_polars=True,
-            backend_version=parse_version(pl.__version__),
+            PolarsDataFrame(native_object, backend_version=parse_version(pl.__version__)),
             level="full",
         )
     elif (pl := get_polars()) is not None and isinstance(native_object, pl.LazyFrame):
@@ -330,9 +323,7 @@ def from_native(  # noqa: PLR0915
             msg = "Cannot only use `eager_only` or `eager_or_interchange_only` with polars.LazyFrame"
             raise TypeError(msg)
         return LazyFrame(
-            native_object,
-            is_polars=True,
-            backend_version=parse_version(pl.__version__),
+            PolarsLazyFrame(native_object, backend_version=parse_version(pl.__version__)),
             level="full",
         )
     elif (pd := get_pandas()) is not None and isinstance(native_object, pd.DataFrame):
@@ -345,8 +336,6 @@ def from_native(  # noqa: PLR0915
                 backend_version=parse_version(pd.__version__),
                 implementation=Implementation.PANDAS,
             ),
-            is_polars=False,
-            backend_version=parse_version(pd.__version__),
             level="full",
         )
     elif (mpd := get_modin()) is not None and isinstance(native_object, mpd.DataFrame):
@@ -359,8 +348,6 @@ def from_native(  # noqa: PLR0915
                 implementation=Implementation.MODIN,
                 backend_version=parse_version(mpd.__version__),
             ),
-            is_polars=False,
-            backend_version=parse_version(mpd.__version__),
             level="full",
         )
     elif (cudf := get_cudf()) is not None and isinstance(  # pragma: no cover
@@ -375,8 +362,6 @@ def from_native(  # noqa: PLR0915
                 implementation=Implementation.CUDF,
                 backend_version=parse_version(cudf.__version__),
             ),
-            is_polars=False,
-            backend_version=parse_version(cudf.__version__),
             level="full",
         )
     elif (pa := get_pyarrow()) is not None and isinstance(native_object, pa.Table):
@@ -385,8 +370,6 @@ def from_native(  # noqa: PLR0915
             raise TypeError(msg)
         return DataFrame(
             ArrowDataFrame(native_object, backend_version=parse_version(pa.__version__)),
-            is_polars=False,
-            backend_version=parse_version(pa.__version__),
             level="full",
         )
     elif hasattr(native_object, "__dataframe__"):
@@ -399,8 +382,6 @@ def from_native(  # noqa: PLR0915
         # placeholder (0,) version here, as we wouldn't use it in this case anyway.
         return DataFrame(
             InterchangeFrame(native_object.__dataframe__()),
-            is_polars=False,
-            backend_version=(0,),
             level="interchange",
         )
     elif hasattr(native_object, "__narwhals_dataframe__"):
@@ -410,8 +391,6 @@ def from_native(  # noqa: PLR0915
         # placeholder (0,) version here, as we wouldn't use it in this case anyway.
         return DataFrame(
             native_object.__narwhals_dataframe__(),
-            is_polars=False,
-            backend_version=(0,),
             level="full",
         )
     elif hasattr(native_object, "__narwhals_lazyframe__"):
@@ -424,8 +403,6 @@ def from_native(  # noqa: PLR0915
         # placeholder (0,) version here, as we wouldn't use it in this case anyway.
         return LazyFrame(
             native_object.__narwhals_lazyframe__(),
-            is_polars=False,
-            backend_version=(0,),
             level="full",
         )
     elif (pl := get_polars()) is not None and isinstance(native_object, pl.Series):
@@ -433,9 +410,7 @@ def from_native(  # noqa: PLR0915
             msg = "Please set `allow_series=True`"
             raise TypeError(msg)
         return Series(
-            native_object,
-            is_polars=True,
-            backend_version=parse_version(pl.__version__),
+            PolarsSeries(native_object, backend_version=parse_version(pl.__version__)),
             level="full",
         )
     elif (pd := get_pandas()) is not None and isinstance(native_object, pd.Series):
@@ -448,8 +423,6 @@ def from_native(  # noqa: PLR0915
                 implementation=Implementation.PANDAS,
                 backend_version=parse_version(pd.__version__),
             ),
-            is_polars=False,
-            backend_version=parse_version(pd.__version__),
             level="full",
         )
     elif (mpd := get_modin()) is not None and isinstance(native_object, mpd.Series):
@@ -462,8 +435,6 @@ def from_native(  # noqa: PLR0915
                 implementation=Implementation.MODIN,
                 backend_version=parse_version(mpd.__version__),
             ),
-            is_polars=False,
-            backend_version=parse_version(mpd.__version__),
             level="full",
         )
     elif (cudf := get_cudf()) is not None and isinstance(
@@ -478,8 +449,6 @@ def from_native(  # noqa: PLR0915
                 implementation=Implementation.CUDF,
                 backend_version=parse_version(cudf.__version__),
             ),
-            is_polars=False,
-            backend_version=parse_version(cudf.__version__),
             level="full",
         )
     elif (pa := get_pyarrow()) is not None and isinstance(native_object, pa.ChunkedArray):
@@ -490,8 +459,6 @@ def from_native(  # noqa: PLR0915
             ArrowSeries(
                 native_object, backend_version=parse_version(pa.__version__), name=""
             ),
-            is_polars=False,
-            backend_version=parse_version(pa.__version__),
             level="full",
         )
     elif hasattr(native_object, "__narwhals_series__"):
@@ -501,8 +468,6 @@ def from_native(  # noqa: PLR0915
         # placeholder (0,) version here, as we wouldn't use it in this case anyway.
         return Series(
             native_object.__narwhals_series__(),
-            backend_version=(0,),
-            is_polars=False,
             level="full",
         )
     elif strict:
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 1f6a9074..c3ab3b06 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import re
+import secrets
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
@@ -85,17 +86,6 @@ def isinstance_or_issubclass(obj: Any, cls: Any) -> bool:
     return isinstance(obj, cls) or issubclass(obj, cls)
 
 
-def validate_same_library(items: Iterable[Any]) -> None:
-    if all(item._is_polars for item in items):
-        return
-    if all(hasattr(item._compliant_frame, "_implementation") for item in items) and (
-        len({item._compliant_frame._implementation for item in items}) == 1
-    ):
-        return
-    msg = "Cross-library comparisons aren't supported"
-    raise NotImplementedError(msg)
-
-
 def validate_laziness(items: Iterable[Any]) -> None:
     from narwhals.dataframe import DataFrame
     from narwhals.dataframe import LazyFrame
@@ -336,3 +326,31 @@ def is_ordered_categorical(series: Series) -> bool:
         return native_series.type.ordered  # type: ignore[no-any-return]
     # If it doesn't match any of the above, let's just play it safe and return False.
     return False  # pragma: no cover
+
+
+def generate_unique_token(n_bytes: int, columns: list[str]) -> str:  # pragma: no cover
+    """Generates a unique token of specified n_bytes that is not present in the given list of columns.
+
+    Arguments:
+        n_bytes : The number of bytes to generate for the token.
+        columns : The list of columns to check for uniqueness.
+
+    Returns:
+        A unique token that is not present in the given list of columns.
+
+    Raises:
+        AssertionError: If a unique token cannot be generated after 100 attempts.
+    """
+    counter = 0
+    while True:
+        token = secrets.token_hex(n_bytes)
+        if token not in columns:
+            return token
+
+        counter += 1
+        if counter > 100:
+            msg = (
+                "Internal Error: Narwhals was not able to generate a column name to perform given "
+                "join operation"
+            )
+            raise AssertionError(msg)
diff --git a/pyproject.toml b/pyproject.toml
index 884d4a68..4849689c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "narwhals"
-version = "1.1.1"
+version = "1.1.2"
 authors = [
   { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
 ]
diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py
index 2d1c36b8..86cc1513 100644
--- a/utils/check_api_reference.py
+++ b/utils/check_api_reference.py
@@ -37,9 +37,7 @@ if extra := set(documented).difference(top_level_functions):
 
 top_level_functions = [
     i
-    for i in nw.DataFrame(
-        pl.DataFrame(), is_polars=True, backend_version=(0,), level="full"
-    ).__dir__()
+    for i in nw.from_native(pl.DataFrame()).__dir__()
     if not i[0].isupper() and i[0] != "_"
 ]
 with open("docs/api-reference/dataframe.md") as fd:
@@ -60,9 +58,7 @@ if extra := set(documented).difference(top_level_functions).difference({"__getit
 
 top_level_functions = [
     i
-    for i in nw.LazyFrame(
-        pl.LazyFrame(), is_polars=True, backend_version=(0,), level="full"
-    ).__dir__()
+    for i in nw.from_native(pl.LazyFrame()).__dir__()
     if not i[0].isupper() and i[0] != "_"
 ]
 with open("docs/api-reference/lazyframe.md") as fd:
@@ -83,9 +79,7 @@ if extra := set(documented).difference(top_level_functions):
 
 top_level_functions = [
     i
-    for i in nw.Series(
-        pl.Series(), backend_version=(1,), is_polars=True, level="full"
-    ).__dir__()
+    for i in nw.from_native(pl.Series(), series_only=True).__dir__()
     if not i[0].isupper() and i[0] != "_"
 ]
 with open("docs/api-reference/series.md") as fd:
@@ -141,9 +135,7 @@ if extra := set(documented).difference(top_level_functions):
 expr = [i for i in nw.Expr(lambda: 0).__dir__() if not i[0].isupper() and i[0] != "_"]
 series = [
     i
-    for i in nw.Series(
-        pl.Series(), backend_version=(1,), is_polars=True, level="full"
-    ).__dir__()
+    for i in nw.from_native(pl.Series(), series_only=True).__dir__()
     if not i[0].isupper() and i[0] != "_"
 ]
 
diff --git a/utils/check_backend_completeness.py b/utils/check_backend_completeness.py
index 90506dc4..1c74f68a 100644
--- a/utils/check_backend_completeness.py
+++ b/utils/check_backend_completeness.py
@@ -18,7 +18,6 @@ MISSING = [
     "DataFrame.pipe",
     "DataFrame.unique",
     "Series.drop_nulls",
-    "Series.fill_null",
     "Series.from_iterable",
     "Series.is_between",
     "Series.is_duplicated",
@@ -26,7 +25,6 @@ MISSING = [
     "Series.is_last_distinct",
     "Series.is_sorted",
     "Series.is_unique",
-    "Series.n_unique",
     "Series.quantile",
     "Series.round",
     "Series.shift",
@@ -79,8 +77,6 @@ if __name__ == "__main__":
     df_pa = ArrowDataFrame(MockDataFrame({"a": [1, 2, 3]}), backend_version=(13, 0))
     df_nw = nw.DataFrame(
         MockDataFrame({"a": [1, 2, 3]}),
-        is_polars=True,
-        backend_version=(1,),
         level="full",
     )
     pa_methods = [f"DataFrame.{x}" for x in df_pa.__dir__() if not x.startswith("_")]


Test patch:
diff --git a/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
index 3ae02c65..871fcadc 100644
--- a/.github/workflows/downstream_tests.yml
+++ b/.github/workflows/downstream_tests.yml
@@ -9,7 +9,7 @@ jobs:
   altair:
     strategy:
       matrix:
-        python-version: ["3.11"]
+        python-version: ["3.12"]
         os: [ubuntu-latest]
 
     runs-on: ${{ matrix.os }}
@@ -27,7 +27,10 @@ jobs:
             ~\AppData\Local\pip\Cache
           key: ${{ runner.os }}-build-${{ matrix.python-version }}
       - name: clone-altair
-        run: git clone https://github.com/vega/altair.git
+        run: |
+            git clone https://github.com/vega/altair.git --depth=1
+            cd altair
+            git log
       - name: install-basics
         run: python -m pip install --upgrade tox virtualenv setuptools pip
       - name: install-altair-dev
@@ -70,7 +73,7 @@ jobs:
             ~\AppData\Local\pip\Cache
           key: ${{ runner.os }}-build-${{ matrix.python-version }}
       - name: clone-scikit-lego
-        run: git clone https://github.com/koaning/scikit-lego.git
+        run: git clone https://github.com/koaning/scikit-lego.git --depth 1
       - name: install-basics
         run: python -m pip install --upgrade tox virtualenv setuptools pip
       - name: install-scikit-lego-dev
diff --git a/tests/expr_and_series/fill_null_test.py b/tests/expr_and_series/fill_null_test.py
index 5cffcddd..dc9cc738 100644
--- a/tests/expr_and_series/fill_null_test.py
+++ b/tests/expr_and_series/fill_null_test.py
@@ -1,7 +1,5 @@
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
@@ -12,10 +10,7 @@ data = {
 }
 
 
-def test_fill_null(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_fill_null(constructor: Any) -> None:
     df = nw.from_native(constructor(data), eager_only=True)
 
     result = df.with_columns(nw.col("a", "b", "c").fill_null(99))
diff --git a/tests/expr_and_series/n_unique_test.py b/tests/expr_and_series/n_unique_test.py
index 291a0a43..98dd0a85 100644
--- a/tests/expr_and_series/n_unique_test.py
+++ b/tests/expr_and_series/n_unique_test.py
@@ -1,7 +1,5 @@
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
@@ -11,10 +9,7 @@ data = {
 }
 
 
-def test_n_unique(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_n_unique(constructor: Any) -> None:
     df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(nw.all().n_unique())
     expected = {
diff --git a/tests/expr_and_series/str/contains_test.py b/tests/expr_and_series/str/contains_test.py
index 32e45de0..831aa82d 100644
--- a/tests/expr_and_series/str/contains_test.py
+++ b/tests/expr_and_series/str/contains_test.py
@@ -2,7 +2,6 @@ from typing import Any
 
 import pandas as pd
 import polars as pl
-import pytest
 
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
@@ -13,9 +12,8 @@ df_pandas = pd.DataFrame(data)
 df_polars = pl.DataFrame(data)
 
 
-@pytest.mark.parametrize("df_any", [df_pandas, df_polars])
-def test_contains(df_any: Any) -> None:
-    df = nw.from_native(df_any, eager_only=True)
+def test_contains(constructor: Any) -> None:
+    df = nw.from_native(constructor(data), eager_only=True)
     result = df.with_columns(
         case_insensitive_match=nw.col("pets").str.contains("(?i)parrot|Dove")
     )
diff --git a/tests/expr_and_series/str/head_test.py b/tests/expr_and_series/str/head_test.py
index 9fe2638f..9deb2862 100644
--- a/tests/expr_and_series/str/head_test.py
+++ b/tests/expr_and_series/str/head_test.py
@@ -1,16 +1,12 @@
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 data = {"a": ["foo", "bars"]}
 
 
-def test_str_head(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
+def test_str_head(constructor: Any) -> None:
     df = nw.from_native(constructor(data), eager_only=True)
     result = df.select(nw.col("a").str.head(3))
     expected = {
diff --git a/tests/expr_and_series/str/slice_test.py b/tests/expr_and_series/str/slice_test.py
index 946f4e35..e3831952 100644
--- a/tests/expr_and_series/str/slice_test.py
+++ b/tests/expr_and_series/str/slice_test.py
@@ -15,11 +15,8 @@ data = {"a": ["fdas", "edfas"]}
     [(1, 2, {"a": ["da", "df"]}), (-2, None, {"a": ["as", "as"]})],
 )
 def test_str_slice(
-    request: Any, constructor: Any, offset: int, length: int | None, expected: Any
+    constructor: Any, offset: int, length: int | None, expected: Any
 ) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     df = nw.from_native(constructor(data), eager_only=True)
     result_frame = df.select(nw.col("a").str.slice(offset, length))
     compare_dicts(result_frame, expected)
diff --git a/tests/expr_and_series/str/starts_with_ends_with_test.py b/tests/expr_and_series/str/starts_with_ends_with_test.py
index ee477461..9216f2e6 100644
--- a/tests/expr_and_series/str/starts_with_ends_with_test.py
+++ b/tests/expr_and_series/str/starts_with_ends_with_test.py
@@ -2,8 +2,6 @@ from __future__ import annotations
 
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 
 # Don't move this into typechecking block, for coverage
@@ -13,10 +11,7 @@ from tests.utils import compare_dicts
 data = {"a": ["fdas", "edfas"]}
 
 
-def test_ends_with(request: Any, constructor_with_lazy: Any) -> None:
-    if "pyarrow_table" in str(constructor_with_lazy):
-        request.applymarker(pytest.mark.xfail)
-
+def test_ends_with(constructor_with_lazy: Any) -> None:
     df = nw.from_native(constructor_with_lazy(data)).lazy()
     result = df.select(nw.col("a").str.ends_with("das"))
     expected = {
@@ -31,10 +26,7 @@ def test_ends_with(request: Any, constructor_with_lazy: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_starts_with(request: Any, constructor_with_lazy: Any) -> None:
-    if "pyarrow_table" in str(constructor_with_lazy):
-        request.applymarker(pytest.mark.xfail)
-
+def test_starts_with(constructor_with_lazy: Any) -> None:
     df = nw.from_native(constructor_with_lazy(data)).lazy()
     result = df.select(nw.col("a").str.starts_with("fda"))
     expected = {
diff --git a/tests/expr_and_series/str/tail_test.py b/tests/expr_and_series/str/tail_test.py
index b7c187e1..f116e445 100644
--- a/tests/expr_and_series/str/tail_test.py
+++ b/tests/expr_and_series/str/tail_test.py
@@ -1,17 +1,12 @@
 from typing import Any
 
-import pytest
-
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
 data = {"a": ["foo", "bars"]}
 
 
-def test_str_tail(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_str_tail(constructor: Any) -> None:
     df = nw.from_native(constructor(data), eager_only=True)
     expected = {"a": ["foo", "ars"]}
 
diff --git a/tests/expr_and_series/str/to_datetime_test.py b/tests/expr_and_series/str/to_datetime_test.py
index 62c438f8..ea248bde 100644
--- a/tests/expr_and_series/str/to_datetime_test.py
+++ b/tests/expr_and_series/str/to_datetime_test.py
@@ -1,19 +1,15 @@
 from datetime import datetime
 from typing import Any
 
-import pandas as pd
-import polars as pl
-import pytest
-
 import narwhals.stable.v1 as nw
 
-df_pandas = pd.DataFrame({"a": ["2020-01-01T12:34:56"]})
-df_polars = pl.DataFrame({"a": ["2020-01-01T12:34:56"]})
+data = {"a": ["2020-01-01T12:34:56"]}
 
 
-@pytest.mark.parametrize("df_any", [df_pandas, df_polars])
-def test_to_datetime(df_any: Any) -> None:
-    result = nw.from_native(df_any, eager_only=True).select(
-        b=nw.col("a").str.to_datetime(format="%Y-%m-%dT%H:%M:%S")
-    )["b"][0]
+def test_to_datetime(constructor: Any) -> None:
+    result = (
+        nw.from_native(constructor(data), eager_only=True)
+        .select(b=nw.col("a").str.to_datetime(format="%Y-%m-%dT%H:%M:%S"))
+        .item(row=0, column="b")
+    )
     assert result == datetime(2020, 1, 1, 12, 34, 56)
diff --git a/tests/expr_and_series/test_dt.py b/tests/expr_and_series/test_dt.py
index eaa7b8bf..1b7399a2 100644
--- a/tests/expr_and_series/test_dt.py
+++ b/tests/expr_and_series/test_dt.py
@@ -139,6 +139,7 @@ def test_duration_micro_nano(
     parse_version(pd.__version__) < parse_version("2.0.0"),
     reason="pyarrow dtype not available",
 )
+@pytest.mark.slow()
 def test_ordinal_day(dates: datetime) -> None:
     result_pd = nw.from_native(pd.Series([dates]), series_only=True).dt.ordinal_day()[0]
     result_pdms = nw.from_native(
@@ -168,6 +169,7 @@ def test_ordinal_day(dates: datetime) -> None:
     parse_version(pd.__version__) < parse_version("2.2.0"),
     reason="pyarrow dtype not available",
 )
+@pytest.mark.slow()
 def test_total_minutes(timedeltas: timedelta) -> None:
     result_pd = nw.from_native(
         pd.Series([timedeltas]), series_only=True
diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py
index 9ba11991..d8ab380c 100644
--- a/tests/frame/join_test.py
+++ b/tests/frame/join_test.py
@@ -15,7 +15,9 @@ def test_inner_join_two_keys(constructor: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data), eager_only=True)
     df_right = df
-    result = df.join(df_right, left_on=["a", "b"], right_on=["a", "b"], how="inner")
+    result = df.lazy().join(
+        df_right.lazy(), left_on=["a", "b"], right_on=["a", "b"], how="inner"
+    )
     expected = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9], "z_right": [7.0, 8, 9]}
     compare_dicts(result, expected)
 
@@ -35,15 +37,11 @@ def test_inner_join_single_key(constructor: Any) -> None:
     compare_dicts(result, expected)
 
 
-def test_cross_join(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_cross_join(constructor: Any) -> None:
     data = {"a": [1, 3, 2]}
     df = nw.from_native(constructor(data))
-    result = df.join(df, how="cross")  # type: ignore[arg-type]
-
-    expected = {"a": [1, 1, 1, 3, 3, 3, 2, 2, 2], "a_right": [1, 3, 2, 1, 3, 2, 1, 3, 2]}
+    result = df.join(df, how="cross").sort("a", "a_right")  # type: ignore[arg-type]
+    expected = {"a": [1, 1, 1, 2, 2, 2, 3, 3, 3], "a_right": [1, 2, 3, 1, 2, 3, 1, 2, 3]}
     compare_dicts(result, expected)
 
     with pytest.raises(ValueError, match="Can not pass left_on, right_on for cross join"):
@@ -69,15 +67,11 @@ def test_cross_join_non_pandas() -> None:
     ],
 )
 def test_anti_join(
-    request: Any,
     constructor: Any,
     join_key: list[str],
     filter_expr: nw.Expr,
     expected: dict[str, list[Any]],
 ) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     other = df.filter(filter_expr)
@@ -94,15 +88,11 @@ def test_anti_join(
     ],
 )
 def test_semi_join(
-    request: Any,
     constructor: Any,
     join_key: list[str],
     filter_expr: nw.Expr,
     expected: dict[str, list[Any]],
 ) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
     other = df.filter(filter_expr)
@@ -125,10 +115,7 @@ def test_join_not_implemented(constructor: Any, how: str) -> None:
 
 
 @pytest.mark.filterwarnings("ignore:the default coalesce behavior")
-def test_left_join(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_left_join(constructor: Any) -> None:
     data_left = {"a": [1.0, 2, 3], "b": [4.0, 5, 6]}
     data_right = {"a": [1.0, 2, 3], "c": [4.0, 5, 7]}
     df_left = nw.from_native(constructor(data_left), eager_only=True)
@@ -141,10 +128,7 @@ def test_left_join(request: Any, constructor: Any) -> None:
 
 
 @pytest.mark.filterwarnings("ignore: the default coalesce behavior")
-def test_left_join_multiple_column(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
+def test_left_join_multiple_column(constructor: Any) -> None:
     data_left = {"a": [1, 2, 3], "b": [4, 5, 6]}
     data_right = {"a": [1, 2, 3], "c": [4, 5, 6]}
     df_left = nw.from_native(constructor(data_left), eager_only=True)
@@ -155,12 +139,9 @@ def test_left_join_multiple_column(request: Any, constructor: Any) -> None:
 
 
 @pytest.mark.filterwarnings("ignore: the default coalesce behavior")
-def test_left_join_overlapping_column(request: Any, constructor: Any) -> None:
-    if "pyarrow_table" in str(constructor):
-        request.applymarker(pytest.mark.xfail)
-
-    data_left = {"a": [1, 2, 3], "b": [4, 5, 6], "d": [1, 4, 2]}
-    data_right = {"a": [1, 2, 3], "c": [4, 5, 6], "d": [1, 4, 2]}
+def test_left_join_overlapping_column(constructor: Any) -> None:
+    data_left = {"a": [1.0, 2, 3], "b": [4.0, 5, 6], "d": [1.0, 4, 2]}
+    data_right = {"a": [1.0, 2, 3], "c": [4.0, 5, 6], "d": [1.0, 4, 2]}
     df_left = nw.from_native(constructor(data_left), eager_only=True)
     df_right = nw.from_native(constructor(data_right), eager_only=True)
     result = df_left.join(df_right, left_on="b", right_on="c", how="left")
diff --git a/tests/frame/schema_test.py b/tests/frame/schema_test.py
index aff89209..37a9d8be 100644
--- a/tests/frame/schema_test.py
+++ b/tests/frame/schema_test.py
@@ -114,7 +114,8 @@ def test_dtypes() -> None:
         "r": nw.Enum,
     }
 
-    assert df_from_pl.schema == df_from_pl.collect_schema() == expected
+    assert df_from_pl.schema == df_from_pl.collect_schema()
+    assert df_from_pl.schema == expected
     assert {name: df_from_pl[name].dtype for name in df_from_pl.columns} == expected
 
     # pandas/pyarrow only have categorical, not enum
diff --git a/tests/frame/test_common.py b/tests/frame/test_common.py
index 03efae10..bf7f619f 100644
--- a/tests/frame/test_common.py
+++ b/tests/frame/test_common.py
@@ -264,27 +264,6 @@ def test_reindex(df_raw: Any) -> None:
         nw.to_native(df.with_columns(nw.all() + nw.all()))
 
 
-@pytest.mark.parametrize(
-    ("c_left", "c_right"),
-    [(pd.DataFrame, pl.DataFrame), (pa.table, pd.DataFrame)],
-)
-def test_library(c_left: Any, c_right: Any) -> None:
-    df_left = nw.from_native(c_left(data)).lazy()
-    df_right = nw.from_native(c_right(data)).lazy()
-    with pytest.raises(
-        NotImplementedError, match="Cross-library comparisons aren't supported"
-    ):
-        nw.concat([df_left, df_right], how="horizontal")
-    with pytest.raises(
-        NotImplementedError, match="Cross-library comparisons aren't supported"
-    ):
-        nw.concat([df_left, df_right], how="vertical")
-    with pytest.raises(
-        NotImplementedError, match="Cross-library comparisons aren't supported"
-    ):
-        df_left.join(df_right, left_on=["a"], right_on=["a"], how="inner")
-
-
 @pytest.mark.parametrize(
     ("row", "column", "expected"),
     [(0, 2, 7), (1, "z", 8)],
diff --git a/tests/hypothesis/test_join.py b/tests/hypothesis/test_join.py
index d5db53e3..850caa1a 100644
--- a/tests/hypothesis/test_join.py
+++ b/tests/hypothesis/test_join.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import pandas as pd
 import polars as pl
+import pyarrow as pa
 import pytest
 from hypothesis import assume
 from hypothesis import given
@@ -163,3 +164,18 @@ def test_left_join(  # pragma: no cover
         )
     ).select(pl.all().fill_null(float("nan")))
     compare_dicts(result_pd.to_dict(as_series=False), result_pl.to_dict(as_series=False))
+    # For PyArrow, insert an extra sort, as the order of rows isn't guaranteed
+    result_pa = (
+        nw.from_native(pa.table(data_left), eager_only=True)
+        .join(
+            nw.from_native(pa.table(data_right), eager_only=True),
+            how="left",
+            left_on=left_key,
+            right_on=right_key,
+        )
+        .select(nw.all().cast(nw.Float64).fill_null(float("nan")))
+        .pipe(lambda df: df.sort(df.columns))
+    )
+    compare_dicts(
+        result_pa, result_pd.pipe(lambda df: df.sort(df.columns)).to_dict(as_series=False)
+    )
diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py
index 7e5b67b2..191ad49b 100644
--- a/tests/translate/from_native_test.py
+++ b/tests/translate/from_native_test.py
@@ -153,118 +153,6 @@ def test_pandas_like_validate() -> None:
         nw.from_native(df)
 
 
-@pytest.mark.parametrize(
-    ("series", "is_polars", "context"),
-    [
-        (MockSeries(), False, does_not_raise()),
-        (MockSeries(), True, does_not_raise()),
-        (series_pl, True, does_not_raise()),
-        (
-            series_pd,
-            False,
-            pytest.raises(
-                TypeError,
-                match="Expected Polars Series or an object which implements `__narwhals_series__`",
-            ),
-        ),
-        (
-            MockDataFrame(),
-            False,
-            pytest.raises(
-                TypeError,
-                match="Expected Polars Series or an object which implements `__narwhals_series__`",
-            ),
-        ),
-    ],
-)
-def test_init_series(series: Any, is_polars: Any, context: Any) -> None:
-    with context:
-        result = nw.Series(
-            series, is_polars=is_polars, backend_version=(1, 2, 3), level="full"
-        )
-        assert isinstance(result, nw.Series)
-
-
-@pytest.mark.parametrize(
-    ("dframe", "is_polars", "context"),
-    [
-        (MockDataFrame(), False, does_not_raise()),
-        (MockDataFrame(), True, does_not_raise()),
-        (df_pl, True, does_not_raise()),
-        (
-            df_pd,
-            False,
-            pytest.raises(
-                TypeError,
-                match="Expected Polars DataFrame or an object which implements `__narwhals_dataframe__`",
-            ),
-        ),
-        (
-            MockLazyFrame(),
-            False,
-            pytest.raises(
-                TypeError,
-                match="Expected Polars DataFrame or an object which implements `__narwhals_dataframe__`",
-            ),
-        ),
-        (
-            MockSeries(),
-            True,
-            pytest.raises(
-                TypeError,
-                match="Expected Polars DataFrame or an object which implements `__narwhals_dataframe__`",
-            ),
-        ),
-    ],
-)
-def test_init_eager(dframe: Any, is_polars: Any, context: Any) -> None:
-    with context:
-        result = nw.DataFrame(
-            dframe, is_polars=is_polars, backend_version=(1, 2, 3), level="full"
-        )  # type: ignore[var-annotated]
-        assert isinstance(result, nw.DataFrame)
-
-
-@pytest.mark.parametrize(
-    ("dframe", "is_polars", "context"),
-    [
-        (MockLazyFrame(), False, does_not_raise()),
-        (MockLazyFrame(), True, does_not_raise()),
-        (lf_pl, True, does_not_raise()),
-        (
-            df_pd,
-            False,
-            pytest.raises(
-                TypeError,
-                match="Expected Polars LazyFrame or an object that implements `__narwhals_lazyframe__`",
-            ),
-        ),
-        (
-            MockDataFrame(),
-            False,
-            pytest.raises(
-                TypeError,
-                match="Expected Polars LazyFrame or an object that implements `__narwhals_lazyframe__`",
-            ),
-        ),
-        (
-            MockSeries(),
-            True,
-            pytest.raises(
-                TypeError,
-                match="Expected Polars LazyFrame or an object that implements `__narwhals_lazyframe__`",
-            ),
-        ),
-    ],
-)
-def test_init_lazy(dframe: Any, is_polars: Any, context: Any) -> None:
-    with context:
-        result = nw.LazyFrame(
-            dframe, is_polars=is_polars, backend_version=(1, 2, 3), level="full"
-        )  # type: ignore[var-annotated]
-        assert isinstance(result, nw.LazyFrame)
-
-
 def test_init_already_narwhals() -> None:
     df = nw.from_native(pl.DataFrame({"a": [1, 2, 3]}))
     result = nw.from_native(df)