Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2afc8c8
feat: Add `Series.from_iterable`
dangotbanned Aug 3, 2025
ba7a983
feat: Add to `v1`, `v2`
dangotbanned Aug 3, 2025
4841d9a
docs: Adapt `Series.from_numpy`
dangotbanned Aug 3, 2025
5c28080
test: Port `new_series` tests
dangotbanned Aug 3, 2025
fdc108b
test: cover failure cases
dangotbanned Aug 3, 2025
d939ef7
docs: Add to api reference
dangotbanned Aug 3, 2025
02ab3cb
test: cover `v1`, `v2`
dangotbanned Aug 3, 2025
1cfcafb
test: don't rely on `pandas` supporting `[us]`
dangotbanned Aug 3, 2025
2a9ac28
test: xfail pandas `[ms]`
dangotbanned Aug 3, 2025
b6e2895
Merge branch 'main' into series-from-iterable
dangotbanned Aug 3, 2025
2137178
Merge remote-tracking branch 'upstream/main' into series-from-iterable
dangotbanned Aug 4, 2025
28da5a0
refactor: fix `RET505`
dangotbanned Aug 4, 2025
7fd179c
test: Split out infer case
dangotbanned Aug 4, 2025
efc5c4e
test: Check lots of iterables
dangotbanned Aug 4, 2025
328ecf0
test(DRAFT): xfail `polars` w/ `pd.array`, todo `pyarrow` fix
dangotbanned Aug 4, 2025
3f13379
fix: Ensure `cast` occurs before `pa.chunked_array` call
dangotbanned Aug 4, 2025
bd2a084
test: Cover other data types
dangotbanned Aug 4, 2025
48cf17b
fix: Try downcasting in `polars`
dangotbanned Aug 4, 2025
9bfd3c4
cov plz
dangotbanned Aug 4, 2025
c666872
fix: backport `pd.Index` support
dangotbanned Aug 4, 2025
4a27299
cov
dangotbanned Aug 4, 2025
f1feba7
test: xfail pandas nightly string polars
dangotbanned Aug 4, 2025
2b70b18
test: Simplify test ids
dangotbanned Aug 4, 2025
f7ab46f
Merge branch 'main' into series-from-iterable
dangotbanned Aug 4, 2025
f965e98
Merge branch 'main' into series-from-iterable
dangotbanned Aug 5, 2025
4646051
test: just skip this nonsense
dangotbanned Aug 5, 2025
58f093a
Merge branch 'main' into series-from-iterable
dangotbanned Aug 7, 2025
dc78c53
Merge branch 'main' into series-from-iterable
dangotbanned Aug 7, 2025
8b1fdc6
Merge branch 'main' into series-from-iterable
dangotbanned Aug 8, 2025
c206b8f
Merge branch 'main' into series-from-iterable
dangotbanned Aug 8, 2025
48cb0b3
Merge branch 'main' into series-from-iterable
dangotbanned Aug 8, 2025
ebfeb1b
Merge branch 'main' into series-from-iterable
dangotbanned Aug 10, 2025
773fcbb
Merge branch 'main' into series-from-iterable
dangotbanned Aug 12, 2025
cd98af6
revert: try *not* downcasting polars
dangotbanned Aug 12, 2025
4400166
fix: backport respect `dtype`, optimize branching
dangotbanned Aug 12, 2025
f73a2f9
Merge remote-tracking branch 'upstream/main' into series-from-iterable
dangotbanned Aug 12, 2025
f0d1a4f
Merge branch 'main' into series-from-iterable
dangotbanned Aug 13, 2025
3fc9e06
Merge branch 'main' into series-from-iterable
dangotbanned Aug 13, 2025
8b31c92
Merge branch 'main' into series-from-iterable
dangotbanned Aug 15, 2025
a30f54f
Merge branch 'main' into series-from-iterable
dangotbanned Aug 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api-reference/series.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- exp
- fill_null
- filter
- from_iterable
- from_numpy
- gather_every
- head
Expand Down
14 changes: 10 additions & 4 deletions narwhals/_arrow/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
chunked_array,
extract_native,
floordiv_compat,
is_array_or_scalar,
lit,
narwhals_to_native_dtype,
native_to_narwhals_dtype,
Expand Down Expand Up @@ -156,10 +157,15 @@ def from_iterable(
dtype: IntoDType | None = None,
) -> Self:
version = context._version
dtype_pa = narwhals_to_native_dtype(dtype, version) if dtype else None
return cls.from_native(
chunked_array([data], dtype_pa), name=name, context=context
)
if dtype is not None:
dtype_pa: pa.DataType | None = narwhals_to_native_dtype(dtype, version)
if is_array_or_scalar(data):
data = data.cast(dtype_pa)
dtype_pa = None
native = data if cls._is_native(data) else chunked_array([data], dtype_pa)
else:
native = chunked_array([data])
return cls.from_native(native, context=context, name=name)

def _from_scalar(self, value: Any) -> Self:
if hasattr(value, "as_py"):
Expand Down
7 changes: 6 additions & 1 deletion narwhals/_arrow/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,19 @@ def extract_py_scalar(value: Any, /) -> Any:
return maybe_extract_py_scalar(value, return_py_scalar=True)


def is_array_or_scalar(obj: Any) -> TypeIs[ArrayOrScalar]:
"""Return True for any base `pyarrow` container."""
return isinstance(obj, (pa.ChunkedArray, pa.Array, pa.Scalar))


def chunked_array(
arr: ArrayOrScalar | list[Iterable[Any]], dtype: pa.DataType | None = None, /
) -> ChunkedArrayAny:
if isinstance(arr, pa.ChunkedArray):
return arr
if isinstance(arr, list):
return pa.chunked_array(arr, dtype)
return pa.chunked_array([arr], arr.type)
return pa.chunked_array([arr], dtype)


def nulls_like(n: int, series: ArrowSeries) -> ArrayAny:
Expand Down
19 changes: 13 additions & 6 deletions narwhals/_polars/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from narwhals._polars.utils import (
BACKEND_VERSION,
SERIES_ACCEPTS_PD_INDEX,
SERIES_RESPECTS_DTYPE,
PolarsAnyNamespace,
PolarsCatNamespace,
PolarsDateTimeNamespace,
Expand All @@ -19,7 +21,7 @@
native_to_narwhals_dtype,
)
from narwhals._utils import Implementation, requires
from narwhals.dependencies import is_numpy_array_1d
from narwhals.dependencies import is_numpy_array_1d, is_pandas_index

if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Mapping, Sequence
Expand Down Expand Up @@ -48,6 +50,7 @@
T = TypeVar("T")
IncludeBreakpoint: TypeAlias = Literal[False, True]

Incomplete: TypeAlias = Any

# Series methods where PolarsSeries just defers to Polars.Series directly.
INHERITED_METHODS = frozenset(
Expand Down Expand Up @@ -180,9 +183,15 @@ def from_iterable(
) -> Self:
version = context._version
dtype_pl = narwhals_to_native_dtype(dtype, version) if dtype else None
# NOTE: `Iterable` is fine, annotation is overly narrow
# https://github.com/pola-rs/polars/blob/82d57a4ee41f87c11ca1b1af15488459727efdd7/py-polars/polars/series/series.py#L332-L333
native = pl.Series(name=name, values=cast("Sequence[Any]", data), dtype=dtype_pl)
values: Incomplete = data
if SERIES_RESPECTS_DTYPE:
native = pl.Series(name, values, dtype=dtype_pl)
else: # pragma: no cover
if (not SERIES_ACCEPTS_PD_INDEX) and is_pandas_index(values):
values = values.to_series()
native = pl.Series(name, values)
if dtype_pl:
native = native.cast(dtype_pl)
Comment on lines +189 to +194
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh boy 😩

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried something new here when thinking about (#2933 (comment)), which might not be visible from the diff alone

  • Keeps the compat code fast (we evaulate this once)
  • Doesn't clutter the impl with comments
  • Can use markdown to explain the problem + link to issue/PR
image image

return cls.from_native(native, context=context)

@staticmethod
Expand Down Expand Up @@ -565,8 +574,6 @@ def _bins_from_bin_count(self, bin_count: int) -> pl.Series: # pragma: no cover
returns bins that range from -inf to +inf and has bin_count + 1 bins.
for compat: convert `bin_count=` call to `bins=`
"""
from typing import cast

lower = cast("float", self.native.min())
upper = cast("float", self.native.max())

Expand Down
11 changes: 10 additions & 1 deletion narwhals/_polars/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import abc
from functools import lru_cache
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar, overload
from typing import TYPE_CHECKING, Any, ClassVar, Final, Protocol, TypeVar, overload

import polars as pl

Expand Down Expand Up @@ -49,6 +49,15 @@
BACKEND_VERSION = Implementation.POLARS._backend_version()
"""Static backend version for `polars`."""

SERIES_RESPECTS_DTYPE: Final[bool] = BACKEND_VERSION >= (0, 20, 26)
"""`pl.Series(dtype=...)` fixed in https://github.com/pola-rs/polars/pull/15962

Includes `SERIES_ACCEPTS_PD_INDEX`.
"""

SERIES_ACCEPTS_PD_INDEX: Final[bool] = BACKEND_VERSION >= (0, 20, 7)
"""`pl.Series(values: pd.Index)` fixed in https://github.com/pola-rs/polars/pull/14087"""


@overload
def extract_native(obj: _StoresNative[NativeT]) -> NativeT: ...
Expand Down
70 changes: 68 additions & 2 deletions narwhals/series.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import math
from collections.abc import Iterator, Mapping, Sequence
from collections.abc import Iterable, Iterator, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, Literal, overload

from narwhals._utils import (
Expand All @@ -13,9 +13,10 @@
is_compliant_series,
is_eager_allowed,
is_index_selector,
qualified_type_name,
supports_arrow_c_stream,
)
from narwhals.dependencies import is_numpy_array_1d, is_numpy_scalar
from narwhals.dependencies import is_numpy_array, is_numpy_array_1d, is_numpy_scalar
from narwhals.dtypes import _validate_dtype, _validate_into_dtype
from narwhals.exceptions import ComputeError, InvalidOperationError
from narwhals.series_cat import SeriesCatNamespace
Expand Down Expand Up @@ -157,6 +158,71 @@ def from_numpy(
)
raise ValueError(msg)

@classmethod
def from_iterable(
cls,
name: str,
values: Iterable[Any],
dtype: IntoDType | None = None,
*,
backend: ModuleType | Implementation | str,
) -> Series[Any]:
"""Construct a Series from an iterable.

Arguments:
name: Name of resulting Series.
values: One-dimensional data represented as an iterable.
dtype: (Narwhals) dtype. If not provided, the native library
may auto-infer it from `values`.
backend: specifies which eager backend instantiate to.

`backend` can be specified in various ways

- As `Implementation.<BACKEND>` with `BACKEND` being `PANDAS`, `PYARROW`,
`POLARS`, `MODIN` or `CUDF`.
- As a string: `"pandas"`, `"pyarrow"`, `"polars"`, `"modin"` or `"cudf"`.
- Directly as a module `pandas`, `pyarrow`, `polars`, `modin` or `cudf`.

Returns:
A new Series

Examples:
>>> import pandas as pd
>>> import narwhals as nw
>>>
>>> values = [4, 1, 3, 2]
>>> nw.Series.from_iterable("a", values, dtype=nw.UInt32, backend="pandas")
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
| Narwhals Series |
|----------------------|
|0 4 |
|1 1 |
|2 3 |
|3 2 |
|Name: a, dtype: uint32|
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
"""
if is_numpy_array(values):
return cls.from_numpy(name, values, dtype, backend=backend)
if dtype:
_validate_into_dtype(dtype)
if not isinstance(values, Iterable):
msg = f"Expected values to be an iterable, got: {qualified_type_name(values)!r}."
raise TypeError(msg)
implementation = Implementation.from_backend(backend)
if is_eager_allowed(implementation):
ns = cls._version.namespace.from_backend(implementation).compliant
compliant = ns._series.from_iterable(
values, context=ns, name=name, dtype=dtype
)
return cls(compliant, level="full")
msg = (
f"{implementation} support in Narwhals is lazy-only, but `Series.from_iterable` is an eager-only function.\n\n"
"Hint: you may want to use an eager backend and then call `.lazy`, e.g.:\n\n"
f" nw.Series.from_iterable('a', [1,2,3], backend='pyarrow').to_frame().lazy('{implementation}')"
)
raise ValueError(msg)

@property
def implementation(self) -> Implementation:
"""Return implementation of native Series.
Expand Down
12 changes: 12 additions & 0 deletions narwhals/stable/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,18 @@ def from_numpy(
result = super().from_numpy(name, values, dtype, backend=backend)
return cast("Series[Any]", result)

@classmethod
def from_iterable(
cls,
name: str,
values: Iterable[Any],
dtype: IntoDType | None = None,
*,
backend: ModuleType | Implementation | str,
) -> Series[Any]:
result = super().from_iterable(name, values, dtype, backend=backend)
return cast("Series[Any]", result)

@property
def _dataframe(self) -> type[DataFrame[Any]]:
return DataFrame
Expand Down
16 changes: 16 additions & 0 deletions narwhals/stable/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ def collect(


class Series(NwSeries[IntoSeriesT]):
_version = Version.V2

@inherit_doc(NwSeries)
def __init__(
self, series: Any, *, level: Literal["full", "lazy", "interchange"]
Expand All @@ -203,6 +205,20 @@ def __init__(
def _dataframe(self) -> type[DataFrame[Any]]:
return DataFrame

# TODO @dangotbanned: Fix `from_numpy` override missing in `v2` in another PR
Copy link
Member Author

@dangotbanned dangotbanned Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

  • Resolve missing v2 method(s) and tests in another PR


@classmethod
def from_iterable(
cls,
name: str,
values: Iterable[Any],
dtype: IntoDType | None = None,
*,
backend: ModuleType | Implementation | str,
) -> Series[Any]:
result = super().from_iterable(name, values, dtype, backend=backend)
return cast("Series[Any]", result)

def to_frame(self) -> DataFrame[Any]:
return _stableify(super().to_frame())

Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,9 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
@pytest.fixture(params=TEST_EAGER_BACKENDS)
def eager_backend(request: pytest.FixtureRequest) -> EagerAllowed:
return request.param # type: ignore[no-any-return]


@pytest.fixture(params=[el for el in TEST_EAGER_BACKENDS if not isinstance(el, str)])
def eager_implementation(request: pytest.FixtureRequest) -> EagerAllowed:
"""Use if a test is heavily parametric, skips `str` backend."""
return request.param # type: ignore[no-any-return]
Loading
Loading