From a11807a6966ab1be2c0c7acce10aede1d7e362c3 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:24:26 +0000 Subject: [PATCH 01/18] tie `IntoFrameT` to `NativeFrame` --- narwhals/_compliant/dataframe.py | 27 +++++++++++---------------- narwhals/_native.py | 4 ++-- narwhals/dataframe.py | 20 ++++++++++---------- narwhals/translate.py | 21 ++++++++++++--------- 4 files changed, 35 insertions(+), 37 deletions(-) diff --git a/narwhals/_compliant/dataframe.py b/narwhals/_compliant/dataframe.py index 3e7810616c..3249947553 100644 --- a/narwhals/_compliant/dataframe.py +++ b/narwhals/_compliant/dataframe.py @@ -12,8 +12,7 @@ CompliantSeriesT, EagerExprT, EagerSeriesT, - NativeDataFrameT, - NativeLazyFrameT, + NativeFrameT, NativeSeriesT, ) from narwhals._translate import ( @@ -179,8 +178,8 @@ class CompliantDataFrame( DictConvertible["_ToDict[CompliantSeriesT]", Mapping[str, Any]], ArrowConvertible["pa.Table", "IntoArrowTable"], Sized, - CompliantFrame[CompliantExprT_contra, NativeDataFrameT, ToNarwhalsT_co], - Protocol[CompliantSeriesT, CompliantExprT_contra, NativeDataFrameT, ToNarwhalsT_co], + CompliantFrame[CompliantExprT_contra, NativeFrameT, ToNarwhalsT_co], + Protocol[CompliantSeriesT, CompliantExprT_contra, NativeFrameT, ToNarwhalsT_co], ): def __narwhals_dataframe__(self) -> Self: ... @classmethod @@ -292,8 +291,8 @@ def write_parquet(self, file: str | Path | BytesIO) -> None: ... class CompliantLazyFrame( - CompliantFrame[CompliantExprT_contra, NativeLazyFrameT, ToNarwhalsT_co], - Protocol[CompliantExprT_contra, NativeLazyFrameT, ToNarwhalsT_co], + CompliantFrame[CompliantExprT_contra, NativeFrameT, ToNarwhalsT_co], + Protocol[CompliantExprT_contra, NativeFrameT, ToNarwhalsT_co], ): def __narwhals_lazyframe__(self) -> Self: ... # `LazySelectorNamespace._iter_columns` depends @@ -312,12 +311,10 @@ def sink_parquet(self, file: str | Path | BytesIO) -> None: ... class EagerDataFrame( - CompliantDataFrame[ - EagerSeriesT, EagerExprT, NativeDataFrameT, "DataFrame[NativeDataFrameT]" - ], - CompliantLazyFrame[EagerExprT, "Incomplete", "DataFrame[NativeDataFrameT]"], + CompliantDataFrame[EagerSeriesT, EagerExprT, NativeFrameT, "DataFrame[NativeFrameT]"], + CompliantLazyFrame[EagerExprT, "Incomplete", "DataFrame[NativeFrameT]"], ValidateBackendVersion, - Protocol[EagerSeriesT, EagerExprT, NativeDataFrameT, NativeSeriesT], + Protocol[EagerSeriesT, EagerExprT, NativeFrameT, NativeSeriesT], ): @property def _backend_version(self) -> tuple[int, ...]: @@ -325,11 +322,9 @@ def _backend_version(self) -> tuple[int, ...]: def __narwhals_namespace__( self, - ) -> EagerNamespace[ - Self, EagerSeriesT, EagerExprT, NativeDataFrameT, NativeSeriesT - ]: ... + ) -> EagerNamespace[Self, EagerSeriesT, EagerExprT, NativeFrameT, NativeSeriesT]: ... - def to_narwhals(self) -> DataFrame[NativeDataFrameT]: + def to_narwhals(self) -> DataFrame[NativeFrameT]: return self._version.dataframe(self, level="full") def aggregate(self, *exprs: EagerExprT) -> Self: # pyright: ignore[reportIncompatibleMethodOverride] @@ -343,7 +338,7 @@ def aggregate(self, *exprs: EagerExprT) -> Self: # pyright: ignore[reportIncomp return self.select(*exprs) # pyright: ignore[reportArgumentType] def _with_native( - self, df: NativeDataFrameT, *, validate_column_names: bool = True + self, df: NativeFrameT, *, validate_column_names: bool = True ) -> Self: ... def _check_columns_exist(self, subset: Sequence[str]) -> ColumnNotFoundError | None: diff --git a/narwhals/_native.py b/narwhals/_native.py index 2e06ac7041..96d5c45d03 100644 --- a/narwhals/_native.py +++ b/narwhals/_native.py @@ -287,7 +287,7 @@ def dropDuplicatesWithinWatermark(self, *arg: Any, **kwargs: Any) -> Any: ... # NativePySparkConnect: TypeAlias = _PySparkDataFrame NativeSparkLike: TypeAlias = "NativeSQLFrame | NativePySpark | NativePySparkConnect" NativeKnown: TypeAlias = "NativePolars | NativeArrow | NativePandasLike | NativeSparkLike | NativeDuckDB | NativeDask | NativeIbis" -NativeUnknown: TypeAlias = "NativeDataFrame | NativeSeries | NativeLazyFrame" +NativeUnknown: TypeAlias = "NativeFrame | NativeSeries" NativeAny: TypeAlias = "NativeKnown | NativeUnknown" IntoDataFrame: TypeAlias = NativeDataFrame @@ -304,7 +304,7 @@ def dropDuplicatesWithinWatermark(self, *arg: Any, **kwargs: Any) -> Any: ... # """ IntoLazyFrame: TypeAlias = Union[NativeLazyFrame, NativeIbis] -IntoFrame: TypeAlias = Union[IntoDataFrame, IntoLazyFrame] +IntoFrame: TypeAlias = NativeFrame """Anything which can be converted to a Narwhals DataFrame or LazyFrame. Use this if your function can accept an object which can be converted to either diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index 5ba416c62b..5e6e2d25f0 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -95,7 +95,7 @@ PS = ParamSpec("PS") Incomplete: TypeAlias = Any -_FrameT = TypeVar("_FrameT", bound="IntoFrame") +FrameT = TypeVar("FrameT", bound="IntoFrame") LazyFrameT = TypeVar("LazyFrameT", bound="IntoLazyFrame") DataFrameT = TypeVar("DataFrameT", bound="IntoDataFrame") R = TypeVar("R") @@ -104,7 +104,7 @@ MultiIndexSelector: TypeAlias = "_MultiIndexSelector[Series[Any]]" -class BaseFrame(Generic[_FrameT]): +class BaseFrame(Generic[FrameT]): _compliant_frame: Any _level: Literal["full", "lazy", "interchange"] @@ -447,7 +447,7 @@ def explode(self, columns: str | Sequence[str], *more_columns: str) -> Self: return self._with_compliant(self._compliant_frame.explode(columns=to_explode)) -class DataFrame(BaseFrame[DataFrameT]): +class DataFrame(BaseFrame[FrameT]): """Narwhals DataFrame, backed by a native eager dataframe. Warning: @@ -475,7 +475,7 @@ class DataFrame(BaseFrame[DataFrameT]): _version: ClassVar[Version] = Version.MAIN @property - def _compliant(self) -> CompliantDataFrame[Any, Any, DataFrameT, Self]: + def _compliant(self) -> CompliantDataFrame[Any, Any, FrameT, Self]: return self._compliant_frame @property @@ -492,7 +492,7 @@ def _validate_metadata(self, metadata: ExprMetadata) -> None: def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: self._level: Literal["full", "lazy", "interchange"] = level - self._compliant_frame: CompliantDataFrame[Any, Any, DataFrameT, Self] + self._compliant_frame: CompliantDataFrame[Any, Any, FrameT, Self] if is_compliant_dataframe(df): self._compliant_frame = df.__narwhals_dataframe__() else: # pragma: no cover @@ -871,7 +871,7 @@ def lazy( msg = f"Not-supported backend.\n\nExpected one of {get_args(_LazyAllowedImpl)} or `None`, got {lazy_backend}" raise ValueError(msg) - def to_native(self) -> DataFrameT: + def to_native(self) -> FrameT: """Convert Narwhals DataFrame to native one. Examples: @@ -2348,7 +2348,7 @@ def explode(self, columns: str | Sequence[str], *more_columns: str) -> Self: return super().explode(columns, *more_columns) -class LazyFrame(BaseFrame[LazyFrameT]): +class LazyFrame(BaseFrame[FrameT]): """Narwhals LazyFrame, backed by a native lazyframe. Warning: @@ -2362,7 +2362,7 @@ class LazyFrame(BaseFrame[LazyFrameT]): """ @property - def _compliant(self) -> CompliantLazyFrame[Any, LazyFrameT, Self]: + def _compliant(self) -> CompliantLazyFrame[Any, FrameT, Self]: return self._compliant_frame @property @@ -2395,7 +2395,7 @@ def _validate_metadata(self, metadata: ExprMetadata) -> None: def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: self._level = level - self._compliant_frame: CompliantLazyFrame[Any, LazyFrameT, Self] + self._compliant_frame: CompliantLazyFrame[Any, FrameT, Self] if is_compliant_lazyframe(df): self._compliant_frame = df.__narwhals_lazyframe__() else: # pragma: no cover @@ -2478,7 +2478,7 @@ def collect( msg = f"Unsupported `backend` value.\nExpected one of {get_args(_LazyFrameCollectImpl)} or None, got: {eager_backend}." raise ValueError(msg) - def to_native(self) -> LazyFrameT: + def to_native(self) -> FrameT: """Convert Narwhals LazyFrame to native one. Examples: diff --git a/narwhals/translate.py b/narwhals/translate.py index 7d59b63dfa..7b7607fe57 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -55,6 +55,7 @@ Frame, IntoDataFrameT, IntoFrame, + IntoFrameT, IntoLazyFrameT, IntoSeries, IntoSeriesT, @@ -70,12 +71,12 @@ @overload def to_native( - narwhals_object: DataFrame[IntoDataFrameT], *, pass_through: Literal[False] = ... -) -> IntoDataFrameT: ... + narwhals_object: DataFrame[IntoFrameT], *, pass_through: Literal[False] = ... +) -> IntoFrameT: ... @overload def to_native( - narwhals_object: LazyFrame[IntoLazyFrameT], *, pass_through: Literal[False] = ... -) -> IntoLazyFrameT: ... + narwhals_object: LazyFrame[IntoFrameT], *, pass_through: Literal[False] = ... +) -> IntoFrameT: ... @overload def to_native( narwhals_object: Series[IntoSeriesT], *, pass_through: Literal[False] = ... @@ -85,12 +86,10 @@ def to_native(narwhals_object: Any, *, pass_through: bool) -> Any: ... def to_native( - narwhals_object: DataFrame[IntoDataFrameT] - | LazyFrame[IntoLazyFrameT] - | Series[IntoSeriesT], + narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series[IntoSeriesT], *, pass_through: bool = False, -) -> IntoDataFrameT | IntoLazyFrameT | IntoSeriesT | Any: +) -> IntoFrameT | IntoSeriesT | Any: """Convert Narwhals object to native one. Arguments: @@ -145,6 +144,10 @@ def from_native( native_object: IntoLazyFrameT, **kwds: Unpack[AllowLazy] ) -> LazyFrame[IntoLazyFrameT]: ... @overload +def from_native( # type: ignore[overload-overlap] + native_object: IntoFrameT, **kwds: Unpack[AllowLazy] +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]: ... +@overload def from_native( native_object: IntoDataFrameT | IntoSeriesT, **kwds: Unpack[AllowSeries] ) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... @@ -164,7 +167,7 @@ def from_native( series_only: bool, allow_series: bool | None, ) -> Any: ... -def from_native( +def from_native( # pyright: ignore[reportInconsistentOverload] native_object: IntoLazyFrameT | IntoDataFrameT | IntoSeriesT From 97999f339ff0ce5c9cadc73f496d472eb534b08c Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:54:42 +0000 Subject: [PATCH 02/18] some extra overloads --- narwhals/translate.py | 12 ++++++++---- tests/translate/from_native_test.py | 16 ++++++++++++++++ tests/utils_test.py | 2 +- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 7b7607fe57..6d44df0339 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -145,13 +145,17 @@ def from_native( ) -> LazyFrame[IntoLazyFrameT]: ... @overload def from_native( # type: ignore[overload-overlap] - native_object: IntoFrameT, **kwds: Unpack[AllowLazy] -) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]: ... -@overload -def from_native( native_object: IntoDataFrameT | IntoSeriesT, **kwds: Unpack[AllowSeries] ) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... @overload +def from_native( + native_object: IntoDataFrameT | IntoLazyFrameT, **kwds: Unpack[AllowLazy] +) -> DataFrame[IntoDataFrameT] | LazyFrame[IntoLazyFrameT]: ... +@overload +def from_native( # type: ignore[overload-overlap] + native_object: IntoFrameT, **kwds: Unpack[AllowLazy] +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]: ... +@overload def from_native( native_object: IntoDataFrameT | IntoLazyFrameT | IntoSeriesT, **kwds: Unpack[AllowAny] ) -> DataFrame[IntoDataFrameT] | LazyFrame[IntoLazyFrameT] | Series[IntoSeriesT]: ... diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index daa626dafe..b12667dda5 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -39,6 +39,8 @@ from _pytest.mark import ParameterSet from typing_extensions import assert_type + from narwhals.typing import IntoFrameT + class MockDataFrame: def __init__(self, version: Version) -> None: @@ -775,3 +777,17 @@ def test_from_native_series_exhaustive() -> None: # noqa: PLR0914, PLR0915 for series in chain(pls, pds, pas): assert isinstance(series, nw.Series) + + +def test_readme_example() -> None: + # check that readme example (as of March 2026) passes + def _agnostic_function( + df_native: IntoFrameT, date_column: str, price_column: str + ) -> IntoFrameT: + return ( + nw.from_native(df_native) + .group_by(nw.col(date_column).dt.truncate("1mo")) + .agg(nw.col(price_column).mean()) + .sort(date_column) + .to_native() + ) diff --git a/tests/utils_test.py b/tests/utils_test.py index b0336e8a04..d93f0c5592 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -130,7 +130,7 @@ def test_maybe_set_index_pandas_direct_index( pandas_index: pd.Series[Any] | list[pd.Series[Any]], native_df_or_series: pd.DataFrame | pd.Series[Any], ) -> None: - df = nw.from_native(native_df_or_series, allow_series=True) + df = nw.from_native(native_df_or_series, allow_series=True, eager_only=True) result = nw.maybe_set_index(df, index=narwhals_index) if isinstance(native_df_or_series, pd.Series): assert isinstance(result, nw.Series) From 9d85f3386b80be8e20b77c2e926e4cca1c9bb9a4 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:19:20 +0000 Subject: [PATCH 03/18] wip: add extra test --- tests/translate/from_native_test.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index b12667dda5..5f68618c49 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, Any, Literal, cast import pytest +from typing_extensions import assert_type import narwhals as nw from narwhals._utils import Version @@ -37,7 +38,6 @@ from collections.abc import Iterable, Iterator from _pytest.mark import ParameterSet - from typing_extensions import assert_type from narwhals.typing import IntoFrameT @@ -791,3 +791,22 @@ def _agnostic_function( .sort(date_column) .to_native() ) + + +def test_from_eager_or_lazy_polars() -> None: + lf = pl.LazyFrame() + df = pl.DataFrame() + either = lf if df.height else df + + r_lf = nw.from_native(lf) + assert_type(r_lf, nw.LazyFrame[pl.LazyFrame]) + r_df = nw.from_native(df) + assert_type(r_df, nw.DataFrame[pl.DataFrame]) + r_either = nw.from_native(either) + assert_type(r_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) + + r2_lf = nw.from_native(r_lf) + assert_type(r2_lf, nw.LazyFrame[pl.LazyFrame]) + r2_df = nw.from_native(r_df) + assert_type(r2_df, nw.DataFrame[pl.DataFrame]) + _r2_either = nw.from_native(r_either) From 890fb170ac29020b7c333a6e066eb4c02e7ee52a Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:33:10 +0000 Subject: [PATCH 04/18] add more overloads + test --- narwhals/translate.py | 4 ++++ tests/translate/from_native_test.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 6d44df0339..203444721f 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -128,6 +128,10 @@ def from_native( @overload def from_native(native_object: LazyFrameT, **kwds: Unpack[AllowLazy]) -> LazyFrameT: ... @overload +def from_native( + native_object: LazyFrameT | DataFrameT, **kwds: Unpack[AllowLazy] +) -> LazyFrameT | DataFrameT: ... +@overload def from_native( native_object: IntoDataFrameT, **kwds: Unpack[ExcludeSeries] ) -> DataFrame[IntoDataFrameT]: ... diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 5f68618c49..efb501bd10 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -809,4 +809,5 @@ def test_from_eager_or_lazy_polars() -> None: assert_type(r2_lf, nw.LazyFrame[pl.LazyFrame]) r2_df = nw.from_native(r_df) assert_type(r2_df, nw.DataFrame[pl.DataFrame]) - _r2_either = nw.from_native(r_either) + r2_either = nw.from_native(r_either) + assert_type(r2_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) From 272bba06ee755747e0d71f7cbd9ae70d73e1370c Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:49:51 +0000 Subject: [PATCH 05/18] compat --- tests/translate/from_native_test.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index efb501bd10..7414299eb4 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -27,7 +27,6 @@ from typing import TYPE_CHECKING, Any, Literal, cast import pytest -from typing_extensions import assert_type import narwhals as nw from narwhals._utils import Version @@ -38,6 +37,7 @@ from collections.abc import Iterable, Iterator from _pytest.mark import ParameterSet + from typing_extensions import assert_type from narwhals.typing import IntoFrameT @@ -799,15 +799,17 @@ def test_from_eager_or_lazy_polars() -> None: either = lf if df.height else df r_lf = nw.from_native(lf) - assert_type(r_lf, nw.LazyFrame[pl.LazyFrame]) r_df = nw.from_native(df) - assert_type(r_df, nw.DataFrame[pl.DataFrame]) r_either = nw.from_native(either) - assert_type(r_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) r2_lf = nw.from_native(r_lf) - assert_type(r2_lf, nw.LazyFrame[pl.LazyFrame]) r2_df = nw.from_native(r_df) - assert_type(r2_df, nw.DataFrame[pl.DataFrame]) r2_either = nw.from_native(r_either) - assert_type(r2_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) + + if TYPE_CHECKING: + assert_type(r_lf, nw.LazyFrame[pl.LazyFrame]) + assert_type(r_df, nw.DataFrame[pl.DataFrame]) + assert_type(r_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) + assert_type(r2_lf, nw.LazyFrame[pl.LazyFrame]) + assert_type(r2_df, nw.DataFrame[pl.DataFrame]) + assert_type(r2_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) From 988e5336aef4ad34205b98443d75d9a024226bdd Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:16:18 +0000 Subject: [PATCH 06/18] cvg --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b2a6b7a73a..41d86562ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -319,6 +319,7 @@ omit = [ ] exclude_also = [ "if sys.version_info() <", + "if TYPE_CHECKING", "if .* is Implementation.(CUDF|MODIN|PYSPARK|PYSPARK_CONNECT|IBIS)", "if .*.is_(cudf|modin|pyspark|pyspark_connect|ibis)", 'if "(cudf|modin|pyspark|ibis)" in', From 870637839274e8fe0dda2a12a1ada7d3bb9718aa Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:17:00 +0000 Subject: [PATCH 07/18] deps --- tests/translate/from_native_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 7414299eb4..2ebf808346 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -794,6 +794,9 @@ def _agnostic_function( def test_from_eager_or_lazy_polars() -> None: + pytest.importorskip("polars") + import polars as pl + lf = pl.LazyFrame() df = pl.DataFrame() either = lf if df.height else df From 4e330a813df0e8059af5535a0402e921bb599636 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:46:44 +0000 Subject: [PATCH 08/18] cvg --- pyproject.toml | 1 - tests/translate/from_native_test.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 41d86562ea..b2a6b7a73a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -319,7 +319,6 @@ omit = [ ] exclude_also = [ "if sys.version_info() <", - "if TYPE_CHECKING", "if .* is Implementation.(CUDF|MODIN|PYSPARK|PYSPARK_CONNECT|IBIS)", "if .*.is_(cudf|modin|pyspark|pyspark_connect|ibis)", 'if "(cudf|modin|pyspark|ibis)" in', diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 2ebf808346..ae455aebf5 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -781,7 +781,7 @@ def test_from_native_series_exhaustive() -> None: # noqa: PLR0914, PLR0915 def test_readme_example() -> None: # check that readme example (as of March 2026) passes - def _agnostic_function( + def _agnostic_function( # pragma: no cover df_native: IntoFrameT, date_column: str, price_column: str ) -> IntoFrameT: return ( From 273960e0d49af940cfa2bcf4291e9169c6cefa46 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:11:54 +0000 Subject: [PATCH 09/18] remove unnecessary `eager_only=True` --- tests/utils_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils_test.py b/tests/utils_test.py index d93f0c5592..b0336e8a04 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -130,7 +130,7 @@ def test_maybe_set_index_pandas_direct_index( pandas_index: pd.Series[Any] | list[pd.Series[Any]], native_df_or_series: pd.DataFrame | pd.Series[Any], ) -> None: - df = nw.from_native(native_df_or_series, allow_series=True, eager_only=True) + df = nw.from_native(native_df_or_series, allow_series=True) result = nw.maybe_set_index(df, index=narwhals_index) if isinstance(native_df_or_series, pd.Series): assert isinstance(result, nw.Series) From 0fa4ce19a81e6c1a4bdec6851f07a0e8484ce8c6 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:19:13 +0100 Subject: [PATCH 10/18] update v2 from_native too --- narwhals/stable/v2/__init__.py | 24 +++++++++++++++--- tests/v2_test.py | 45 +++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/narwhals/stable/v2/__init__.py b/narwhals/stable/v2/__init__.py index 3319489a87..543505e179 100644 --- a/narwhals/stable/v2/__init__.py +++ b/narwhals/stable/v2/__init__.py @@ -98,6 +98,8 @@ from narwhals.typing import ( IntoDType, IntoExpr, + IntoFrame, + IntoFrameT, IntoSchema, NonNestedLiteral, PythonLiteral, @@ -111,8 +113,10 @@ P = ParamSpec("P") R = TypeVar("R") +FrameT = TypeVar("FrameT", bound="IntoFrame") -class DataFrame(NwDataFrame[IntoDataFrameT]): + +class DataFrame(NwDataFrame[FrameT]): _version = Version.V2 @inherit_doc(NwDataFrame) @@ -240,7 +244,7 @@ def is_unique(self) -> Series[Any]: return _stableify(super().is_unique()) -class LazyFrame(NwLazyFrame[IntoLazyFrameT]): +class LazyFrame(NwLazyFrame[FrameT]): @inherit_doc(NwLazyFrame) def __init__(self, df: Any, *, level: Literal["full", "lazy", "interchange"]) -> None: assert df._version is Version.V2 # noqa: S101 @@ -395,6 +399,10 @@ def from_native( @overload def from_native(native_object: LazyFrameT, **kwds: Unpack[AllowLazy]) -> LazyFrameT: ... @overload +def from_native( + native_object: LazyFrameT | DataFrameT, **kwds: Unpack[AllowLazy] +) -> LazyFrameT | DataFrameT: ... +@overload def from_native( native_object: IntoDataFrameT, **kwds: Unpack[ExcludeSeries] ) -> DataFrame[IntoDataFrameT]: ... @@ -411,10 +419,18 @@ def from_native( native_object: IntoLazyFrameT, **kwds: Unpack[AllowLazy] ) -> LazyFrame[IntoLazyFrameT]: ... @overload -def from_native( +def from_native( # type: ignore[overload-overlap] native_object: IntoDataFrameT | IntoSeriesT, **kwds: Unpack[AllowSeries] ) -> DataFrame[IntoDataFrameT] | Series[IntoSeriesT]: ... @overload +def from_native( + native_object: IntoDataFrameT | IntoLazyFrameT, **kwds: Unpack[AllowLazy] +) -> DataFrame[IntoDataFrameT] | LazyFrame[IntoLazyFrameT]: ... +@overload +def from_native( # type: ignore[overload-overlap] + native_object: IntoFrameT, **kwds: Unpack[AllowLazy] +) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]: ... +@overload def from_native( native_object: IntoDataFrameT | IntoLazyFrameT | IntoSeriesT, **kwds: Unpack[AllowAny] ) -> DataFrame[IntoDataFrameT] | LazyFrame[IntoLazyFrameT] | Series[IntoSeriesT]: ... @@ -430,7 +446,7 @@ def from_native( series_only: bool, allow_series: bool | None, ) -> Any: ... -def from_native( +def from_native( # pyright: ignore[reportInconsistentOverload] native_object: IntoLazyFrameT | IntoDataFrameT | IntoSeriesT diff --git a/tests/v2_test.py b/tests/v2_test.py index e423449fb5..36648e6a5e 100644 --- a/tests/v2_test.py +++ b/tests/v2_test.py @@ -30,7 +30,7 @@ from typing_extensions import assert_type from narwhals._typing import EagerAllowed - from narwhals.stable.v2.typing import IntoDataFrameT + from narwhals.stable.v2.typing import IntoDataFrameT, IntoFrameT from narwhals.typing import IntoDType, _1DArray, _2DArray @@ -544,3 +544,46 @@ def test_first_last() -> None: result = df.select(b=nw_v2.col("a").first(), c=nw_v2.col("a").last()) expected = {"b": [0], "c": [-1]} assert_equal_data(result, expected) + + +def test_readme_example() -> None: + # check that readme example (as of March 2026) passes + def _agnostic_function( # pragma: no cover + df_native: IntoFrameT, date_column: str, price_column: str + ) -> IntoFrameT: + return ( + nw_v2.from_native(df_native) + .group_by(nw_v2.col(date_column).dt.truncate("1mo")) + .agg(nw_v2.col(price_column).mean()) + .sort(date_column) + .to_native() + ) + + +def test_from_eager_or_lazy_polars() -> None: + pytest.importorskip("polars") + import polars as pl + + lf = pl.LazyFrame() + df = pl.DataFrame() + either = lf if df.height else df + + r_lf = nw_v2.from_native(lf) + r_df = nw_v2.from_native(df) + r_either = nw_v2.from_native(either) + + r2_lf = nw_v2.from_native(r_lf) + r2_df = nw_v2.from_native(r_df) + r2_either = nw_v2.from_native(r_either) + + if TYPE_CHECKING: + assert_type(r_lf, nw_v2.LazyFrame[pl.LazyFrame]) + assert_type(r_df, nw_v2.DataFrame[pl.DataFrame]) + assert_type( + r_either, nw_v2.DataFrame[pl.DataFrame] | nw_v2.LazyFrame[pl.LazyFrame] + ) + assert_type(r2_lf, nw_v2.LazyFrame[pl.LazyFrame]) + assert_type(r2_df, nw_v2.DataFrame[pl.DataFrame]) + assert_type( + r2_either, nw_v2.DataFrame[pl.DataFrame] | nw_v2.LazyFrame[pl.LazyFrame] + ) From 22520ac96709e7319d23f1ba036f1f3502db7b15 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:41:11 +0100 Subject: [PATCH 11/18] test some incompatible apis --- narwhals/dataframe.py | 2 +- tests/translate/from_native_test.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index 5e6e2d25f0..2f91d31a73 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -3254,7 +3254,7 @@ def join_asof( suffix=suffix, ) - def lazy(self) -> Self: + def lazy(self) -> LazyFrame[Any]: """Restrict available API methods to lazy-only ones. This is a no-op, and exists only for compatibility with `DataFrame.lazy`. diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index ae455aebf5..6dcc0a10fd 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -816,3 +816,20 @@ def test_from_eager_or_lazy_polars() -> None: assert_type(r2_lf, nw.LazyFrame[pl.LazyFrame]) assert_type(r2_df, nw.DataFrame[pl.DataFrame]) assert_type(r2_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) + + +def test_into_frame_t_incompatible_apis() -> None: + def _agnostic_function( # pragma: no cover + df_native: IntoFrameT, + ) -> IntoFrameT: + nw.from_native(df_native).sink_parquet("...") # type: ignore[union-attr] + _ = ( + nw.from_native(df_native) + .with_row_index() # type: ignore[call-arg] + .to_native() + ) + return ( + nw.from_native(df_native) + .unique(maintain_order=True) # type: ignore[call-arg] + .to_native() + ) From ca8e0ac8f1f289efc2923cf1539c7f241366ed55 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:59:48 +0100 Subject: [PATCH 12/18] revert LazyFrame.lazy -> LazyFrame[Any] --- narwhals/dataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index 2f91d31a73..5e6e2d25f0 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -3254,7 +3254,7 @@ def join_asof( suffix=suffix, ) - def lazy(self) -> LazyFrame[Any]: + def lazy(self) -> Self: """Restrict available API methods to lazy-only ones. This is a no-op, and exists only for compatibility with `DataFrame.lazy`. From cc089ed11b99e7813a15b5ac2b09c1f10071acfa Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:50:50 +0100 Subject: [PATCH 13/18] split out into_frame_t tests --- tests/translate/from_native_test.py | 58 ------------------------ tests/translate/into_frame_t_test.py | 68 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 58 deletions(-) create mode 100644 tests/translate/into_frame_t_test.py diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 6dcc0a10fd..daa626dafe 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -39,8 +39,6 @@ from _pytest.mark import ParameterSet from typing_extensions import assert_type - from narwhals.typing import IntoFrameT - class MockDataFrame: def __init__(self, version: Version) -> None: @@ -777,59 +775,3 @@ def test_from_native_series_exhaustive() -> None: # noqa: PLR0914, PLR0915 for series in chain(pls, pds, pas): assert isinstance(series, nw.Series) - - -def test_readme_example() -> None: - # check that readme example (as of March 2026) passes - def _agnostic_function( # pragma: no cover - df_native: IntoFrameT, date_column: str, price_column: str - ) -> IntoFrameT: - return ( - nw.from_native(df_native) - .group_by(nw.col(date_column).dt.truncate("1mo")) - .agg(nw.col(price_column).mean()) - .sort(date_column) - .to_native() - ) - - -def test_from_eager_or_lazy_polars() -> None: - pytest.importorskip("polars") - import polars as pl - - lf = pl.LazyFrame() - df = pl.DataFrame() - either = lf if df.height else df - - r_lf = nw.from_native(lf) - r_df = nw.from_native(df) - r_either = nw.from_native(either) - - r2_lf = nw.from_native(r_lf) - r2_df = nw.from_native(r_df) - r2_either = nw.from_native(r_either) - - if TYPE_CHECKING: - assert_type(r_lf, nw.LazyFrame[pl.LazyFrame]) - assert_type(r_df, nw.DataFrame[pl.DataFrame]) - assert_type(r_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) - assert_type(r2_lf, nw.LazyFrame[pl.LazyFrame]) - assert_type(r2_df, nw.DataFrame[pl.DataFrame]) - assert_type(r2_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) - - -def test_into_frame_t_incompatible_apis() -> None: - def _agnostic_function( # pragma: no cover - df_native: IntoFrameT, - ) -> IntoFrameT: - nw.from_native(df_native).sink_parquet("...") # type: ignore[union-attr] - _ = ( - nw.from_native(df_native) - .with_row_index() # type: ignore[call-arg] - .to_native() - ) - return ( - nw.from_native(df_native) - .unique(maintain_order=True) # type: ignore[call-arg] - .to_native() - ) diff --git a/tests/translate/into_frame_t_test.py b/tests/translate/into_frame_t_test.py new file mode 100644 index 0000000000..c808ab5fe6 --- /dev/null +++ b/tests/translate/into_frame_t_test.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +import narwhals as nw + +if TYPE_CHECKING: + from typing_extensions import assert_type + + from narwhals.typing import IntoFrameT + + +def test_readme_example() -> None: + # check that readme example (as of March 2026) passes + def _agnostic_function( # pragma: no cover + df_native: IntoFrameT, date_column: str, price_column: str + ) -> IntoFrameT: + return ( + nw.from_native(df_native) + .group_by(nw.col(date_column).dt.truncate("1mo")) + .agg(nw.col(price_column).mean()) + .sort(date_column) + .to_native() + ) + + +def test_from_eager_or_lazy_polars() -> None: + pytest.importorskip("polars") + import polars as pl + + lf = pl.LazyFrame() + df = pl.DataFrame() + either = lf if df.height else df + + r_lf = nw.from_native(lf) + r_df = nw.from_native(df) + r_either = nw.from_native(either) + + r2_lf = nw.from_native(r_lf) + r2_df = nw.from_native(r_df) + r2_either = nw.from_native(r_either) + + if TYPE_CHECKING: + assert_type(r_lf, nw.LazyFrame[pl.LazyFrame]) + assert_type(r_df, nw.DataFrame[pl.DataFrame]) + assert_type(r_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) + assert_type(r2_lf, nw.LazyFrame[pl.LazyFrame]) + assert_type(r2_df, nw.DataFrame[pl.DataFrame]) + assert_type(r2_either, nw.DataFrame[pl.DataFrame] | nw.LazyFrame[pl.LazyFrame]) + + +def test_into_frame_t_incompatible_apis() -> None: + def _agnostic_function( # pragma: no cover + df_native: IntoFrameT, + ) -> IntoFrameT: + nw.from_native(df_native).sink_parquet("...") # type: ignore[union-attr] + _ = ( + nw.from_native(df_native) + .with_row_index() # type: ignore[call-arg] + .to_native() + ) + return ( + nw.from_native(df_native) + .unique(maintain_order=True) # type: ignore[call-arg] + .to_native() + ) From 30cadf4999154df57c954a59f024ceeed0eabe82 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:54:34 +0100 Subject: [PATCH 14/18] add more assert_types to `test_readme_example` --- tests/translate/into_frame_t_test.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/translate/into_frame_t_test.py b/tests/translate/into_frame_t_test.py index c808ab5fe6..2f62fe67a7 100644 --- a/tests/translate/into_frame_t_test.py +++ b/tests/translate/into_frame_t_test.py @@ -3,13 +3,10 @@ from typing import TYPE_CHECKING import pytest +from typing_extensions import assert_type import narwhals as nw - -if TYPE_CHECKING: - from typing_extensions import assert_type - - from narwhals.typing import IntoFrameT +from narwhals.typing import IntoFrameT def test_readme_example() -> None: @@ -17,13 +14,17 @@ def test_readme_example() -> None: def _agnostic_function( # pragma: no cover df_native: IntoFrameT, date_column: str, price_column: str ) -> IntoFrameT: - return ( - nw.from_native(df_native) - .group_by(nw.col(date_column).dt.truncate("1mo")) + df = nw.from_native(df_native) + assert_type(df, nw.DataFrame[IntoFrameT] | nw.LazyFrame[IntoFrameT]) + res = ( + df.group_by(nw.col(date_column).dt.truncate("1mo")) .agg(nw.col(price_column).mean()) .sort(date_column) - .to_native() ) + assert_type(res, nw.DataFrame[IntoFrameT] | nw.LazyFrame[IntoFrameT]) + native = res.to_native() + assert_type(native, IntoFrameT) + return res.to_native() def test_from_eager_or_lazy_polars() -> None: From 287be0a38e157b8ff8030a91a23955a184726888 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:19:40 +0100 Subject: [PATCH 15/18] compat --- tests/translate/into_frame_t_test.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/translate/into_frame_t_test.py b/tests/translate/into_frame_t_test.py index 2f62fe67a7..98876bd841 100644 --- a/tests/translate/into_frame_t_test.py +++ b/tests/translate/into_frame_t_test.py @@ -3,10 +3,13 @@ from typing import TYPE_CHECKING import pytest -from typing_extensions import assert_type import narwhals as nw -from narwhals.typing import IntoFrameT + +if TYPE_CHECKING: + from typing_extensions import assert_type + + from narwhals.typing import IntoFrameT def test_readme_example() -> None: @@ -15,15 +18,18 @@ def _agnostic_function( # pragma: no cover df_native: IntoFrameT, date_column: str, price_column: str ) -> IntoFrameT: df = nw.from_native(df_native) - assert_type(df, nw.DataFrame[IntoFrameT] | nw.LazyFrame[IntoFrameT]) + if TYPE_CHECKING: + assert_type(df, nw.DataFrame[IntoFrameT] | nw.LazyFrame[IntoFrameT]) res = ( df.group_by(nw.col(date_column).dt.truncate("1mo")) .agg(nw.col(price_column).mean()) .sort(date_column) ) - assert_type(res, nw.DataFrame[IntoFrameT] | nw.LazyFrame[IntoFrameT]) + if TYPE_CHECKING: + assert_type(res, nw.DataFrame[IntoFrameT] | nw.LazyFrame[IntoFrameT]) native = res.to_native() - assert_type(native, IntoFrameT) + if TYPE_CHECKING: + assert_type(native, IntoFrameT) return res.to_native() From 78516e1aa32696efb01eaa9a925324abc3597308 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:12:35 +0100 Subject: [PATCH 16/18] remove unnecessary var-annotated ignore --- tests/translate/from_native_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py index 8d076699c0..deb36ccd0c 100644 --- a/tests/translate/from_native_test.py +++ b/tests/translate/from_native_test.py @@ -255,7 +255,7 @@ def test_init_already_narwhals_stable_to_unstable() -> None: s = native["a"] stable_s = nw_v1.from_native(s, allow_series=True) # type: ignore[var-annotated] - unstablified_s = nw.from_native(stable_s, allow_series=True) # type: ignore[var-annotated] + unstablified_s = nw.from_native(stable_s, allow_series=True) assert isinstance(unstablified_s, nw.Series) From 44add0d41316b5ff17d65bbee94bbcd4d6e058e8 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:25:30 +0100 Subject: [PATCH 17/18] broaden union and return type in non-overloaded definition --- narwhals/translate.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/narwhals/translate.py b/narwhals/translate.py index 7133eccaf0..5957a10239 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -175,9 +175,10 @@ def from_native( series_only: bool, allow_series: bool | None, ) -> Any: ... -def from_native( # pyright: ignore[reportInconsistentOverload] - native_object: IntoLazyFrameT +def from_native( # type: ignore[misc] + native_object: IntoFrameT | IntoDataFrameT + | IntoLazyFrameT | IntoSeriesT | IntoFrame | IntoSeries @@ -187,7 +188,14 @@ def from_native( # pyright: ignore[reportInconsistentOverload] eager_only: bool = False, series_only: bool = False, allow_series: bool | None = None, -) -> LazyFrame[IntoLazyFrameT] | DataFrame[IntoDataFrameT] | Series[IntoSeriesT] | T: +) -> ( + LazyFrame[IntoLazyFrameT] + | DataFrame[IntoDataFrameT] + | LazyFrame[IntoFrameT] + | DataFrame[IntoFrameT] + | Series[IntoSeriesT] + | T +): """Convert `native_object` to Narwhals Dataframe, Lazyframe, or Series. Arguments: From a32fb68cfabf99a0de3f8314a1d9bd26f1629eb2 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:41:44 +0100 Subject: [PATCH 18/18] v2 too --- narwhals/stable/v2/__init__.py | 14 +++++++++++--- narwhals/translate.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/narwhals/stable/v2/__init__.py b/narwhals/stable/v2/__init__.py index de8e8d99de..285147f596 100644 --- a/narwhals/stable/v2/__init__.py +++ b/narwhals/stable/v2/__init__.py @@ -448,8 +448,9 @@ def from_native( series_only: bool, allow_series: bool | None, ) -> Any: ... -def from_native( # pyright: ignore[reportInconsistentOverload] - native_object: IntoLazyFrameT +def from_native( + native_object: IntoFrameT + | IntoLazyFrameT | IntoDataFrameT | IntoSeriesT | IntoFrame @@ -460,7 +461,14 @@ def from_native( # pyright: ignore[reportInconsistentOverload] eager_only: bool = False, series_only: bool = False, allow_series: bool | None = None, -) -> LazyFrame[IntoLazyFrameT] | DataFrame[IntoDataFrameT] | Series[IntoSeriesT] | T: +) -> ( + LazyFrame[IntoLazyFrameT] + | DataFrame[IntoDataFrameT] + | LazyFrame[IntoFrameT] + | DataFrame[IntoFrameT] + | Series[IntoSeriesT] + | T +): """Convert `native_object` to Narwhals Dataframe, Lazyframe, or Series. Arguments: diff --git a/narwhals/translate.py b/narwhals/translate.py index 5957a10239..f9ef32461b 100644 --- a/narwhals/translate.py +++ b/narwhals/translate.py @@ -177,8 +177,8 @@ def from_native( ) -> Any: ... def from_native( # type: ignore[misc] native_object: IntoFrameT - | IntoDataFrameT | IntoLazyFrameT + | IntoDataFrameT | IntoSeriesT | IntoFrame | IntoSeries