Repository: narwhals-dev/narwhals
Pull request #2432 (2025-04-25 14:07:42)

refactor: remove `Implementation._alias` in favor of non-auto values
## Related
- #2324 
- #2371

## What type of PR is this? (check all applicable)

- [x] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Checklist

-  [x] Code follows style guide (ruff)
- [ ] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below

This is a breaking change on paper, yet ~one~ two less method to keep in sync 

dangotbanned: @FBruzzesi this isn't an objection, just wanted to say I don't think we need to class this as breaking 🙂 

For all we know [`auto()`](https://docs.python.org/3/library/enum.html#enum.auto) could be returning anything.
Our docs don't even display the value https://narwhals-dev.github.io/narwhals/api-reference/implementation/
dangotbanned: https://github.com/narwhals-dev/narwhals/pull/2432#pullrequestreview-2794743074

@EdAbati have you checked out (#2324)?

I needed to add all the lowercase strings there, which would be simplified if the `Enum` used the same representation 🙂
FBruzzesi: > @FBruzzesi this isn't an objection, just wanted to say I don't think we need to class this as breaking 🙂

That's good to hear!

> For all we know [`auto()`](https://docs.python.org/3/library/enum.html#enum.auto) could be returning anything. Our docs don't even display the value https://narwhals-dev.github.io/narwhals/api-reference/implementation/

Yeah I also think that no one is using/should be rely on these values as (by faulty design), UNKNOWN keeps changing each time we add a new backend.

> I am wondering if we could even go further and make the strings consistent with the ones in the dictionary inside the from_string method.

@EdAbati I thought about it (which is what `StrEnum` would do out of the box) - I don't have a strong opinion on it, and it might be worth it for the sake of simplicity (of a class which is mostly of internal use)
FBruzzesi: @dangotbanned I got a [fix for the CI](https://github.com/narwhals-dev/narwhals/pull/2440)
FBruzzesi: > But they _don't_ show up in `dir()`? 🤔

Mmmh this is odd! I can definitly see all the string methods in `dir(...)` and can call them 

<details><summary> screenshot </summary>

![image](https://github.com/user-attachments/assets/df8217ce-a12e-4d47-b1a6-4fd9932f3b6c)

</details>

> And aren't there at runtime?
> 
> ```python
> >>> nw.Implementation.POLARS.casefold()
> AttributeError: 'Implementation' object has no attribute 'casefold'
> ```

and can call them

![image](https://github.com/user-attachments/assets/1c539067-e2cb-4946-affb-ca5f6dea59ab)


dangotbanned: https://github.com/narwhals-dev/narwhals/pull/2432#issuecomment-2840194090

I'm not at my pc now, but I was running `3.13`.
Are you using something below `3.11` by chance?
FBruzzesi: > Okay so I know I added `StrEnum` in ([261a994](https://github.com/narwhals-dev/narwhals/commit/261a99419158a285f7fb554d6e8c52bdb5432eec)). But how about we just replace that with our own (e.g. `StrLikeEnum` or a better name) that has the new `Enum` behavior but without the `str` subclass?
> 
> The `auto()` part had it's downsides ([#2432 (comment)](https://github.com/narwhals-dev/narwhals/pull/2432#discussion_r2066019463)) but can be preserved if we want. The stuff in `__new__` I don't think _requires_ mixing in `str`.
> 
> I did a full read-through of (https://docs.python.org/3/howto/enum.html) again last night. I think the main reason we'd use a `str` (data) mix-in - is _if_ **we wanted to inherit methods**. My view is **we don't** - since `Implementation` has a lot of methods that we'd want to prioritize.

Thanks @dangotbanned , yes I think that it's ok to avoid using `StrEnum` - the main advantage was allowing `auto()` out of the box, however as we want to have more control over the values it's ok to not use it.

If we ever need some string method we can always add that later on
MarcoGorelli: I haven't managed to follow everything (quite short on time lately 😩 ) but some thoughts I'd like to share are:

**backwards compatibility**

What we absolutely need to preserve is:
```python
df: nw.DataFrame
s = nw.new_series('a', [1,2,3], implementation=df.implementation)
```
or whatever the exact syntax is

It's true that `UNKNOWN` has been changing between versions, but I think that's the only one, right? Could we just assign say `999` to `UNKNOWN` and keep the others stable?

On board with removing `auto` anyway, it came from a community contribution and i should probably have pushed back on it
dangotbanned: > **backwards compatibility**
> 
> What we absolutely need to preserve is:
> 
> ```python
> df: nw.DataFrame
> s = nw.new_series('a', [1,2,3], implementation=df.implementation)
> ```
> 
> or whatever the exact syntax is

That part is all good 👍 

> It's true that `UNKNOWN` has been changing between versions, but I think that's the only one, right? Could we just assign say `999` to `UNKNOWN` and keep the others stable?

@MarcoGorelli while I was testing out how the typing works for the new (`str`) `_value_`(s), the only thing I discovered that *could* break is:

```py
import narwhals as nw

>>> nw.Implementation.PANDAS
<Implementation.PANDAS: 1>

>>> nw.Implementation(1)
<Implementation.PANDAS: 1>
```

We haven't documented this and our tests haven't caught anyone doing it, so I don't think there's a huge concern.

You already can't do much with the `int` value, e.g.:

```py
import narwhals as nw

>>> nw.Implementation.PANDAS == 1
False

>>> nw.Implementation.PANDAS is 1
<>:3: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
<>:3: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
[C:\Users\danie\AppData\Local\Temp\ipykernel_22668\3208834624.py:3](file:///C:/Users/danie/AppData/Local/Temp/ipykernel_22668/3208834624.py:3): SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
  nw.Implementation.PANDAS is 1
False
```

Places we ask for an `Implementation` don't accept an `int`:

```py
import narwhals as nw

>>> nw.new_series("a", [1, 2, 3], backend=1)
File /GitHub/narwhals/narwhals/utils.py:372, in Implementation.to_native_namespace(self)
    369     return pyspark.sql
    371 msg = "Not supported Implementation"  # pragma: no cover
--> 372 raise AssertionError(msg)

AssertionError: Not supported Implementation
```

Patch:
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 553dda1e..cbb4d351 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -108,7 +108,7 @@ class ArrowDataFrame(
         elif backend_version >= (14,) or isinstance(data, Collection):
             native = pa.table(data)
         elif supports_arrow_c_stream(data):  # pragma: no cover
-            msg = f"PyArrow>=14.0.0 is required for `from_arrow` for object of type {type(data).__name__!r}."
+            msg = f"'pyarrow>=14.0.0' is required for `from_arrow` for object of type {type(data).__name__!r}."
             raise ModuleNotFoundError(msg)
         else:  # pragma: no cover
             msg = f"`from_arrow` is not supported for object of type {type(data).__name__!r}."
diff --git a/narwhals/_dask/namespace.py b/narwhals/_dask/namespace.py
index 45c6cd89..8bbf64fd 100644
--- a/narwhals/_dask/namespace.py
+++ b/narwhals/_dask/namespace.py
@@ -5,6 +5,7 @@ from functools import reduce
 from typing import TYPE_CHECKING
 from typing import Iterable
 from typing import Sequence
+from typing import cast
 
 import dask.dataframe as dd
 import pandas as pd
@@ -17,8 +18,6 @@ from narwhals._dask.dataframe import DaskLazyFrame
 from narwhals._dask.expr import DaskExpr
 from narwhals._dask.selectors import DaskSelectorNamespace
 from narwhals._dask.utils import align_series_full_broadcast
-from narwhals._dask.utils import name_preserving_div
-from narwhals._dask.utils import name_preserving_sum
 from narwhals._dask.utils import narwhals_to_native_dtype
 from narwhals._dask.utils import validate_comparand
 from narwhals._expression_parsing import combine_alias_output_names
@@ -184,12 +183,9 @@ class DaskNamespace(
             non_na = align_series_full_broadcast(
                 df, *(1 - s.isna() for s in expr_results)
             )
-            return [
-                name_preserving_div(
-                    reduce(name_preserving_sum, series),
-                    reduce(name_preserving_sum, non_na),
-                )
-            ]
+            num = reduce(lambda x, y: x + y, series)  # pyright: ignore[reportOperatorIssue]
+            den = reduce(lambda x, y: x + y, non_na)  # pyright: ignore[reportOperatorIssue]
+            return [cast("dx.Series", num / den)]  # pyright: ignore[reportOperatorIssue]
 
         return self._expr(
             call=func,
diff --git a/narwhals/_dask/utils.py b/narwhals/_dask/utils.py
index 475154a1..cf740345 100644
--- a/narwhals/_dask/utils.py
+++ b/narwhals/_dask/utils.py
@@ -159,11 +159,3 @@ def narwhals_to_native_dtype(dtype: DType | type[DType], version: Version) -> An
 
     msg = f"Unknown dtype: {dtype}"  # pragma: no cover
     raise AssertionError(msg)
-
-
-def name_preserving_sum(s1: dx.Series, s2: dx.Series) -> dx.Series:
-    return (s1 + s2).rename(s1.name)  # pyright: ignore[reportOperatorIssue]
-
-
-def name_preserving_div(s1: dx.Series, s2: dx.Series) -> dx.Series:
-    return (s1 / s2).rename(s1.name)  # pyright: ignore[reportOperatorIssue]
diff --git a/narwhals/_duckdb/dataframe.py b/narwhals/_duckdb/dataframe.py
index e08757fd..c1e34d26 100644
--- a/narwhals/_duckdb/dataframe.py
+++ b/narwhals/_duckdb/dataframe.py
@@ -247,7 +247,7 @@ class DuckDBLazyFrame(
         if parse_version(pd) >= (1, 0, 0):
             return self.native.df()
         else:  # pragma: no cover
-            msg = f"Conversion to pandas requires pandas>=1.0.0, found {pd.__version__}"
+            msg = f"Conversion to pandas requires 'pandas>=1.0.0', found {pd.__version__}"
             raise NotImplementedError(msg)
 
     def to_arrow(self) -> pa.Table:
@@ -292,7 +292,7 @@ class DuckDBLazyFrame(
 
         if native_how == "cross":
             if self._backend_version < (1, 1, 4):
-                msg = f"DuckDB>=1.1.4 is required for cross-join, found version: {self._backend_version}"
+                msg = f"'duckdb>=1.1.4' is required for cross-join, found version: {self._backend_version}"
                 raise NotImplementedError(msg)
             rel = self.native.set_alias("lhs").cross(other.native.set_alias("rhs"))
         else:
diff --git a/narwhals/_duckdb/expr.py b/narwhals/_duckdb/expr.py
index a216a3fe..81723d52 100644
--- a/narwhals/_duckdb/expr.py
+++ b/narwhals/_duckdb/expr.py
@@ -678,7 +678,7 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "duckdb.Expression"]):
     ) -> Self:
         if strategy is not None:
             if self._backend_version < (1, 3):  # pragma: no cover
-                msg = f"`fill_null` with `strategy={strategy}` is only available in DuckDB>=1.3.0."
+                msg = f"`fill_null` with `strategy={strategy}` is only available in 'duckdb>=1.3.0'."
                 raise NotImplementedError(msg)
 
             def _fill_with_strategy(window_inputs: WindowInputs) -> duckdb.Expression:
diff --git a/narwhals/_enum.py b/narwhals/_enum.py
new file mode 100644
index 00000000..929eecca
--- /dev/null
+++ b/narwhals/_enum.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+# ruff: noqa: ARG004
+from enum import Enum
+from typing import Any
+
+
+class NoAutoEnum(Enum):
+    """Enum base class that prohibits the use of enum.auto() for value assignment.
+
+    This behavior is achieved by overriding the value generation mechanism.
+
+    Examples:
+        >>> from enum import auto
+        >>> from narwhals._enum import NoAutoEnum
+        >>>
+        >>> class Colors(NoAutoEnum):
+        ...     RED = 1
+        ...     GREEN = 2
+        >>> Colors.RED
+        <Colors.RED: 1>
+
+        >>> class ColorsWithAuto(NoAutoEnum):
+        ...     RED = 1
+        ...     GREEN = auto()
+        Traceback (most recent call last):
+        ...
+        ValueError: Creating values with `auto()` is not allowed. Please provide a value manually instead.
+
+    Raises:
+        ValueError: If `auto()` is attempted to be used for any enum member value.
+    """
+
+    @staticmethod
+    def _generate_next_value_(
+        name: str, start: int, count: int, last_values: list[Any]
+    ) -> Any:
+        msg = "Creating values with `auto()` is not allowed. Please provide a value manually instead."
+        raise ValueError(msg)
+
+
+__all__ = ["NoAutoEnum"]
diff --git a/narwhals/_interchange/dataframe.py b/narwhals/_interchange/dataframe.py
index a96fba16..4e5581fd 100644
--- a/narwhals/_interchange/dataframe.py
+++ b/narwhals/_interchange/dataframe.py
@@ -119,7 +119,7 @@ class InterchangeFrame:
         else:  # pragma: no cover
             msg = (
                 "Conversion to pandas is achieved via interchange protocol which requires"
-                f" pandas>=1.5.0 to be installed, found {pd.__version__}"
+                f" 'pandas>=1.5.0' to be installed, found {pd.__version__}"
             )
             raise NotImplementedError(msg)
 
diff --git a/narwhals/_namespace.py b/narwhals/_namespace.py
index d1a5d20e..c5ac14bd 100644
--- a/narwhals/_namespace.py
+++ b/narwhals/_namespace.py
@@ -60,7 +60,7 @@ if TYPE_CHECKING:
     _Dask: TypeAlias = Literal["dask"]
     _DuckDB: TypeAlias = Literal["duckdb"]
     _PandasLike: TypeAlias = Literal["pandas", "cudf", "modin"]
-    _SparkLike: TypeAlias = Literal["pyspark", "sqlframe"]
+    _SparkLike: TypeAlias = Literal["pyspark", "sqlframe", "pyspark[connect]"]
     _EagerOnly: TypeAlias = "_PandasLike | _Arrow"
     _EagerAllowed: TypeAlias = "_Polars | _EagerOnly"
     _LazyOnly: TypeAlias = "_SparkLike | _Dask | _DuckDB"
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index cac4ea8c..c8aaae32 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -941,7 +941,7 @@ class PandasLikeDataFrame(
         if self._implementation is Implementation.PANDAS and (
             self._backend_version < (1, 1)
         ):  # pragma: no cover
-            msg = "pivot is only supported for pandas>=1.1"
+            msg = "pivot is only supported for 'pandas>=1.1'"
             raise NotImplementedError(msg)
         if self._implementation is Implementation.MODIN:
             msg = "pivot is not supported for Modin backend due to https://github.com/modin-project/modin/issues/7409."
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 9ad4efa2..19b14bb0 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -507,7 +507,7 @@ def narwhals_to_native_dtype(  # noqa: PLR0915
         try:
             import pyarrow as pa  # ignore-banned-import
         except ModuleNotFoundError:  # pragma: no cover
-            msg = "PyArrow>=11.0.0 is required for `Date` dtype."
+            msg = "'pyarrow>=11.0.0' is required for `Date` dtype."
         return "date32[pyarrow]"
     if isinstance_or_issubclass(dtype, dtypes.Enum):
         if version is Version.V1:
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
index 4dcd0be6..a25f072b 100644
--- a/narwhals/_polars/dataframe.py
+++ b/narwhals/_polars/dataframe.py
@@ -276,7 +276,7 @@ class PolarsDataFrame:
         self, dtype: Any | None = None, *, copy: bool | None = None
     ) -> _2DArray:
         if self._backend_version < (0, 20, 28) and copy is not None:
-            msg = "`copy` in `__array__` is only supported for Polars>=0.20.28"
+            msg = "`copy` in `__array__` is only supported for 'polars>=0.20.28'"
             raise NotImplementedError(msg)
         if self._backend_version < (0, 20, 28):
             return self.native.__array__(dtype)
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 534a1ff4..adcdcc1d 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -479,7 +479,7 @@ class DataFrame(BaseFrame[DataFrameT]):
             >>> df_native = pd.DataFrame({"a": [1, 2, 3]})
             >>> df = nw.from_native(df_native)
             >>> df.implementation
-            <Implementation.PANDAS: 1>
+            <Implementation.PANDAS: 'pandas'>
             >>> df.implementation.is_pandas()
             True
             >>> df.implementation.is_pandas_like()
@@ -513,10 +513,10 @@ class DataFrame(BaseFrame[DataFrameT]):
         try:
             import pyarrow as pa  # ignore-banned-import
         except ModuleNotFoundError as exc:  # pragma: no cover
-            msg = f"PyArrow>=14.0.0 is required for `DataFrame.__arrow_c_stream__` for object of type {type(native_frame)}"
+            msg = f"'pyarrow>=14.0.0' is required for `DataFrame.__arrow_c_stream__` for object of type {type(native_frame)}"
             raise ModuleNotFoundError(msg) from exc
         if parse_version(pa) < (14, 0):  # pragma: no cover
-            msg = f"PyArrow>=14.0.0 is required for `DataFrame.__arrow_c_stream__` for object of type {type(native_frame)}"
+            msg = f"'pyarrow>=14.0.0' is required for `DataFrame.__arrow_c_stream__` for object of type {type(native_frame)}"
             raise ModuleNotFoundError(msg) from None
         pa_table = self.to_arrow()
         return pa_table.__arrow_c_stream__(requested_schema=requested_schema)  # type: ignore[no-untyped-call]
@@ -2258,7 +2258,7 @@ class LazyFrame(BaseFrame[FrameT]):
             >>> import dask.dataframe as dd
             >>> lf_native = dd.from_dict({"a": [1, 2]}, npartitions=1)
             >>> nw.from_native(lf_native).implementation
-            <Implementation.DASK: 7>
+            <Implementation.DASK: 'dask'>
         """
         return self._compliant_frame._implementation
 
diff --git a/narwhals/series.py b/narwhals/series.py
index 1495ddb9..eacc0635 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -115,7 +115,7 @@ class Series(Generic[IntoSeriesT]):
             >>> s = nw.from_native(s_native, series_only=True)
 
             >>> s.implementation
-            <Implementation.PANDAS: 1>
+            <Implementation.PANDAS: 'pandas'>
 
             >>> s.implementation.is_pandas()
             True
@@ -212,10 +212,10 @@ class Series(Generic[IntoSeriesT]):
         try:
             import pyarrow as pa  # ignore-banned-import
         except ModuleNotFoundError as exc:  # pragma: no cover
-            msg = f"PyArrow>=16.0.0 is required for `Series.__arrow_c_stream__` for object of type {type(native_series)}"
+            msg = f"'pyarrow>=16.0.0' is required for `Series.__arrow_c_stream__` for object of type {type(native_series)}"
             raise ModuleNotFoundError(msg) from exc
         if parse_version(pa) < (16, 0):  # pragma: no cover
-            msg = f"PyArrow>=16.0.0 is required for `Series.__arrow_c_stream__` for object of type {type(native_series)}"
+            msg = f"'pyarrow>=16.0.0' is required for `Series.__arrow_c_stream__` for object of type {type(native_series)}"
             raise ModuleNotFoundError(msg)
         from narwhals._arrow.utils import chunked_array
 
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 498d3a1f..cb73c316 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -565,10 +565,12 @@ def _from_native_impl(  # noqa: PLR0915
     elif is_native_spark_like(native_object):  # pragma: no cover
         ns_spark = version.namespace.from_native_object(native_object)
         if series_only:
-            msg = f"Cannot only use `series_only` with {ns_spark.implementation._alias} DataFrame"
+            msg = (
+                f"Cannot only use `series_only` with {ns_spark.implementation} DataFrame"
+            )
             raise TypeError(msg)
         if eager_only or eager_or_interchange_only:
-            msg = f"Cannot only use `eager_only` or `eager_or_interchange_only` with {ns_spark.implementation._alias} DataFrame"
+            msg = f"Cannot only use `eager_only` or `eager_or_interchange_only` with {ns_spark.implementation} DataFrame"
             raise TypeError(msg)
         return ns_spark.compliant.from_native(native_object).to_narwhals()
 
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 81cc3dbf..9011d547 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -26,6 +26,7 @@ from typing import cast
 from typing import overload
 from warnings import warn
 
+from narwhals._enum import NoAutoEnum
 from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_dask
 from narwhals.dependencies import get_dask_dataframe
@@ -246,35 +247,37 @@ class Version(Enum):
         return SeriesV1
 
 
-class Implementation(Enum):
+class Implementation(NoAutoEnum):
     """Implementation of native object (pandas, Polars, PyArrow, ...)."""
 
-    PANDAS = auto()
-    """Pandas implementation."""
-    MODIN = auto()
+    PANDAS = "pandas"
+    """pandas implementation."""
+    MODIN = "modin"
     """Modin implementation."""
-    CUDF = auto()
+    CUDF = "cudf"
     """cuDF implementation."""
-    PYARROW = auto()
+    PYARROW = "pyarrow"
     """PyArrow implementation."""
-    PYSPARK = auto()
+    PYSPARK = "pyspark"
     """PySpark implementation."""
-    POLARS = auto()
+    POLARS = "polars"
     """Polars implementation."""
-    DASK = auto()
+    DASK = "dask"
     """Dask implementation."""
-    DUCKDB = auto()
+    DUCKDB = "duckdb"
     """DuckDB implementation."""
-    IBIS = auto()
+    IBIS = "ibis"
     """Ibis implementation."""
-    SQLFRAME = auto()
+    SQLFRAME = "sqlframe"
     """SQLFrame implementation."""
-    PYSPARK_CONNECT = auto()
+    PYSPARK_CONNECT = "pyspark[connect]"
     """PySpark Connect implementation."""
-
-    UNKNOWN = auto()
+    UNKNOWN = "unknown"
     """Unknown implementation."""
 
+    def __str__(self) -> str:
+        return str(self.value)
+
     @classmethod
     def from_native_namespace(
         cls: type[Self], native_namespace: ModuleType
@@ -314,20 +317,10 @@ class Implementation(Enum):
         Returns:
             Implementation.
         """
-        mapping = {
-            "pandas": Implementation.PANDAS,
-            "modin": Implementation.MODIN,
-            "cudf": Implementation.CUDF,
-            "pyarrow": Implementation.PYARROW,
-            "pyspark": Implementation.PYSPARK,
-            "polars": Implementation.POLARS,
-            "dask": Implementation.DASK,
-            "duckdb": Implementation.DUCKDB,
-            "ibis": Implementation.IBIS,
-            "sqlframe": Implementation.SQLFRAME,
-            "pyspark_connect": Implementation.PYSPARK_CONNECT,
-        }
-        return mapping.get(backend_name, Implementation.UNKNOWN)
+        try:
+            return cls(backend_name)
+        except ValueError:
+            return Implementation.UNKNOWN
 
     @classmethod
     def from_backend(
@@ -395,9 +388,9 @@ class Implementation(Enum):
             return sqlframe
 
         if self is Implementation.PYSPARK_CONNECT:  # pragma: no cover
-            import pyspark.sql  # ignore-banned-import
+            import pyspark.sql.connect  # ignore-banned-import
 
-            return pyspark.sql
+            return pyspark.sql.connect
 
         msg = "Not supported Implementation"  # pragma: no cover
         raise AssertionError(msg)
@@ -618,28 +611,6 @@ class Implementation(Enum):
         """
         return self is Implementation.SQLFRAME  # pragma: no cover
 
-    @property
-    def _alias(self) -> LiteralString:
-        """Friendly name for errors.
-
-        Returns:
-            String.
-        """
-        mapping: dict[Implementation, LiteralString] = {
-            Implementation.PANDAS: "Pandas",
-            Implementation.POLARS: "Polars",
-            Implementation.DASK: "Dask",
-            Implementation.IBIS: "Ibis",
-            Implementation.MODIN: "Modin",
-            Implementation.CUDF: "cuDF",
-            Implementation.PYARROW: "PyArrow",
-            Implementation.PYSPARK: "PySpark",
-            Implementation.DUCKDB: "DuckDB",
-            Implementation.SQLFRAME: "SQLFrame",
-            Implementation.PYSPARK_CONNECT: "PySpark Connect",
-        }
-        return mapping[self]
-
     def _backend_version(self) -> tuple[int, ...]:
         native = self.to_native_namespace()
         into_version: Any
@@ -1740,7 +1711,7 @@ def _into_arrow_table(data: IntoArrowTable, context: _FullContext, /) -> pa.Tabl
         ns = ArrowNamespace(backend_version=parse_version(pa), version=version)
         return ns._dataframe.from_arrow(data, context=ns).native
     else:  # pragma: no cover
-        msg = f"PyArrow>=14.0.0 is required for `from_arrow` for object of type {type(data).__name__!r}."
+        msg = f"'pyarrow>=14.0.0' is required for `from_arrow` for object of type {type(data).__name__!r}."
         raise ModuleNotFoundError(msg)
 
 
@@ -1900,7 +1871,7 @@ class requires:  # noqa: N801
         >>> backend.really_complex_feature()
         Traceback (most recent call last):
             ...
-        NotImplementedError: `really_complex_feature` is only available in PyArrow>='9000.0.0', found version '20.0.0'.
+        NotImplementedError: `really_complex_feature` is only available in 'pyarrow>=9000.0.0', found version '20.0.0'.
     """
 
     _min_version: tuple[int, ...]
@@ -1930,10 +1901,10 @@ class requires:  # noqa: N801
         if instance._backend_version >= self._min_version:
             return
         method = self._wrapped_name
-        backend = instance._implementation._alias
+        backend = instance._implementation
         minimum = self._unparse_version(self._min_version)
         found = self._unparse_version(instance._backend_version)
-        msg = f"`{method}` is only available in {backend}>={minimum!r}, found version {found!r}."
+        msg = f"`{method}` is only available in '{backend}>={minimum}', found version {found!r}."
         if self._hint:
             msg = f"{msg}\n{self._hint}"
         raise NotImplementedError(msg)


Test patch:
diff --git a/tests/dtypes_test.py b/tests/dtypes_test.py
index d1f403b5..88d48b0a 100644
--- a/tests/dtypes_test.py
+++ b/tests/dtypes_test.py
@@ -470,7 +470,7 @@ def test_enum_repr() -> None:
     result = nw.Enum(["a", "b"])
     assert "Enum(categories=['a', 'b'])" in repr(result)
     result = nw.Enum(nw.Implementation)
-    assert "Enum(categories=[1, 2, 3" in repr(result)
+    assert "Enum(categories=['pandas', 'modin', 'cudf'" in repr(result)
 
 
 def test_enum_hash() -> None:
diff --git a/tests/enum_test.py b/tests/enum_test.py
new file mode 100644
index 00000000..a922329e
--- /dev/null
+++ b/tests/enum_test.py
@@ -0,0 +1,17 @@
+from __future__ import annotations
+
+from enum import auto
+
+import pytest
+
+from narwhals._enum import NoAutoEnum
+
+
+def test_no_auto_enum() -> None:
+    with pytest.raises(
+        ValueError,
+        match=r"Creating values with `auto\(\)` is not allowed. Please provide a value manually instead.",
+    ):
+
+        class TestWithAuto(NoAutoEnum):
+            AUTO = auto()
diff --git a/tests/expr_and_series/nth_test.py b/tests/expr_and_series/nth_test.py
index 22f44934..0c8a6801 100644
--- a/tests/expr_and_series/nth_test.py
+++ b/tests/expr_and_series/nth_test.py
@@ -40,6 +40,6 @@ def test_nth_not_supported() -> None:  # pragma: no cover
     import polars as pl
 
     df = nw.from_native(pl.DataFrame(data))
-    pattern = re.compile(r"`nth` .+ Polars>=\'1.0.0\'.+`col`.+instead", re.DOTALL)
+    pattern = re.compile(r"`nth` .+ \'polars>=1.0.0\'.+`col`.+instead", re.DOTALL)
     with pytest.raises(NotImplementedError, match=pattern):
         df.select(nw.nth(0))
diff --git a/tests/implementation_test.py b/tests/implementation_test.py
index b4c05cd4..c6f7ccff 100644
--- a/tests/implementation_test.py
+++ b/tests/implementation_test.py
@@ -31,3 +31,24 @@ def test_implementation_polars() -> None:
     ].implementation.is_pandas()
     assert nw.from_native(pl.DataFrame({"a": [1, 2, 3]})).implementation.is_polars()
     assert nw.from_native(pl.LazyFrame({"a": [1, 2, 3]})).implementation.is_polars()
+
+
+@pytest.mark.parametrize(
+    ("member", "value"),
+    [
+        ("PANDAS", "pandas"),
+        ("MODIN", "modin"),
+        ("CUDF", "cudf"),
+        ("PYARROW", "pyarrow"),
+        ("PYSPARK", "pyspark"),
+        ("POLARS", "polars"),
+        ("DASK", "dask"),
+        ("DUCKDB", "duckdb"),
+        ("IBIS", "ibis"),
+        ("SQLFRAME", "sqlframe"),
+        ("PYSPARK_CONNECT", "pyspark[connect]"),
+        ("UNKNOWN", "unknown"),
+    ],
+)
+def test_implementation_new(member: str, value: str) -> None:
+    assert nw.Implementation(value) is getattr(nw.Implementation, member)
diff --git a/tests/utils_test.py b/tests/utils_test.py
index 4339bd91..c48f34a9 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -541,15 +541,15 @@ def test_requires() -> None:
 
     converted = v_201.to_int()
     assert converted == 123
-    match = r"`to_int`.+Polars>=\'1.0.0\'.+found.+\'0.5\'"
+    match = r"`to_int`.+\'polars>=1.0.0\'.+found.+\'0.5\'"
     with pytest.raises(NotImplementedError, match=match):
         v_05.to_int()
     repeated = v_300.repeat(3)
     assert repeated == "123123123"
-    match = r"`repeat`.+Polars>=\'3.0.0\'.+found.+\'2.0.1\'"
+    match = r"`repeat`.+\'polars>=3.0.0\'.+found.+\'2.0.1\'"
     with pytest.raises(NotImplementedError, match=match):
         v_201.repeat(3)
-    match = r"`repeat`.+Polars>=\'3.0.0\'.+found.+\'0.5\'"
+    match = r"`repeat`.+\'polars>=3.0.0\'.+found.+\'0.5\'"
     with pytest.raises(NotImplementedError, match=match):
         v_05.repeat(3)
     joined = v_201.concat("456", "789")
@@ -558,7 +558,7 @@ def test_requires() -> None:
     assert joined_sep == "123 456 789"
     assert v_300.concat("forever") == "123forever"
     pattern = re.compile(
-        r"`concat`.+Polars>=\'2\'.+found.+\'0.5\'.+Something helpful I suppose", re.DOTALL
+        r"`concat`.+\'polars>=2\'.+found.+\'0.5\'.+Something helpful I suppose", re.DOTALL
     )
     with pytest.raises(NotImplementedError, match=pattern):
         v_05.concat("never")