Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
89 changes: 88 additions & 1 deletion pandas-stubs/core/series.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,74 @@ from pandas.core.dtypes.dtypes import CategoricalDtype
from pandas.plotting import PlotAccessor

MaskTypeNoList: TypeAlias = Series[bool] | np_ndarray_bool
DisallowedDateTimeDtype: TypeAlias = Literal[
# timedelta
"timedelta64[Y]",
"timedelta64[M]",
"timedelta64[W]",
"timedelta64[D]",
"timedelta64[h]",
"timedelta64[m]",
"timedelta64[μs]",
"timedelta64[ps]",
"timedelta64[fs]",
"timedelta64[as]",
# numpy type codes
"m8[Y]",
"m8[M]",
"m8[W]",
"m8[D]",
"m8[h]",
"m8[m]",
"m8[μs]",
"m8[ps]",
"m8[fs]",
"m8[as]",
# little endian
"<m8[Y]",
"<m8[M]",
"<m8[W]",
"<m8[D]",
"<m8[h]",
"<m8[m]",
"<m8[μs]",
"<m8[ps]",
"<m8[fs]",
"<m8[as]",
# datetime
"datetime64[Y]",
"datetime64[M]",
"datetime64[W]",
"datetime64[D]",
"datetime64[h]",
"datetime64[m]",
"datetime64[μs]",
"datetime64[ps]",
"datetime64[fs]",
"datetime64[as]",
# numpy type codes
"M8[Y]",
"M8[M]",
"M8[W]",
"M8[D]",
"M8[h]",
"M8[m]",
"M8[μs]",
"M8[ps]",
"M8[fs]",
"M8[as]",
# little endian
"<M8[Y]",
"<M8[M]",
"<M8[W]",
"<M8[D]",
"<M8[h]",
"<M8[m]",
"<M8[μs]",
"<M8[ps]",
"<M8[fs]",
"<M8[as]",
]

@type_check_only
class _SupportsAdd(Protocol[T_co]):
Expand Down Expand Up @@ -360,7 +428,7 @@ class Series(IndexOpsMixin[S1], ElementOpsMixin[S1], NDFrame):
copy: bool | None = None,
) -> Series[list[_str]]: ...
@overload
def __new__(
def __new__( # type: ignore[overload-overlap]
cls,
data: Sequence[_str],
index: AxesData | None = None,
Expand Down Expand Up @@ -508,6 +576,25 @@ class Series(IndexOpsMixin[S1], ElementOpsMixin[S1], NDFrame):
copy: bool | None = None,
) -> Self: ...
@overload
def __new__(
cls,
data: (
S1
| ArrayLike
| dict[_str, np_ndarray]
| Sequence[S1]
| IndexOpsMixin[S1]
| dict[HashableT1, S1]
| KeysView[S1]
| ValuesView[S1]
),
index: AxesData | None = None,
*,
dtype: DisallowedDateTimeDtype,
name: Hashable = None,
copy: bool | None = None,
) -> Never: ...
@overload
def __new__(
cls,
data: (
Expand Down
3 changes: 2 additions & 1 deletion pandas-stubs/core/strings/accessor.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import re
from typing import (
Generic,
Literal,
Never,
TypeVar,
overload,
)
Expand Down Expand Up @@ -54,7 +55,7 @@ class StringMethods(
):
def __init__(self, data: T) -> None: ...
def __getitem__(self, key: _slice | int) -> _T_STR: ...
def __iter__(self) -> _T_STR: ...
def __iter__(self) -> Never: ...
@overload
def cat(
self,
Expand Down
7 changes: 7 additions & 0 deletions tests/series/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -3062,6 +3062,13 @@ def test_series_str_methods() -> None:
check(assert_type(s_str.str.lower(), "pd.Series[str]"), pd.Series, str)


def test_series_str_methods_iter() -> None:
"""Test that StringMethods are not iterable."""
s_str = pd.Series(["a", "b"])
if TYPE_CHECKING_INVALID_USAGE:
assert_type(s_str.str.__iter__(), Never)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
assert_type(s_str.str.__iter__(), Never)
assert_type(iter(s_str.str), Never)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For some reason that resolves to Any and not Never, curious if you have insights

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here is a minimal script:

from typing import Any, Generic, Never, TypeVar, reveal_type

class Series:
    @property
    def str(self) -> StringMethods[Any, Series]: ...

_T_EXPANDING = TypeVar("_T_EXPANDING")
_T_STR = TypeVar("_T_STR", bound=Series)

class StringMethods(Generic[_T_EXPANDING, _T_STR]):
    def __getitem__(self, key: int) -> _T_STR: ...
    def __iter__(self) -> Never: ...

def _0() -> None:
    reveal_type(StringMethods().__iter__())  # all 4 type checkers give Never

def _1() -> None:
    reveal_type(iter(Series().str))  # mypy: Any, pyright, pyrefly: Never, ty: Unknown

I think it's worth reporting, but I don't have time right now. @Dr-Irv do you have further insights? I think the real use case is the proposed new version.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Opened astral-sh/ty#3041 to get some insights on the ty case, based on their response that may give us a hint for why mypy has issues.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@loicdiridollou I think you can open up one with mypy as well and see what they have to say.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

python/mypy#21027
The example that they reduced it to on the ty thread works with mypy though:

from typing import Never, Protocol, reveal_type

class ReturnsT[T](Protocol):
    def get(self) -> T: ...

class ReturnsNever:
    def get(self) -> Never:
        raise RuntimeError

def through_protocol[T](x: ReturnsT[T]) -> T:
    return x.get()

reveal_type(through_protocol(ReturnsNever()))

with https://mypy-play.net/ but ty still struggles.



def test_series_explode() -> None:
"""Test Series.explode method."""
s = pd.Series([[1, 2, 3], "foo", [], [3, 4]])
Expand Down
192 changes: 190 additions & 2 deletions tests/series/timedelta/test_dtypes.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,206 @@
from __future__ import annotations

import datetime
from typing import (
TYPE_CHECKING,
Never,
assert_type,
)

import pandas as pd
import pytest

from tests import check
from tests._typing import TimedeltaDtypeArg
from tests import (
TYPE_CHECKING_INVALID_USAGE,
check,
)
from tests._typing import (
TimedeltaDtypeArg,
)
from tests.dtypes import ASTYPE_TIMEDELTA_ARGS


def test_series_construction_timedelta_dtype() -> None:
"""
Test allowable resolutions for pd.Series() construction with timedelta64 dtype.

Only s, ms, us, ns resolutions are valid for Series construction.
Resolutions Y, M, W, D, h, m, μs, ps, fs, as are only valid for astype().
"""

# numpy timedelta64: only s, ms, us, ns are valid for construction
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[s]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[ms]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[us]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[ns]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
# numpy timedelta64 type codes
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="m8[s]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="m8[ms]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="m8[us]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="m8[ns]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
# little-endian numpy timedelta64 type codes
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="<m8[s]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="<m8[ms]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="<m8[us]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)
check(
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="<m8[ns]"),
"pd.Series[pd.Timedelta]",
),
pd.Series,
pd.Timedelta,
)

# PANDAS_UNITS (Y, M, W, D, h, m, μs, ps, fs, as) are not valid for
# Series construction — only for astype(). Using them gives Series[timedelta]
# instead of Series[Timedelta], indicating the dtype is unsupported.

if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[Y]"), Never
)


def test_dtype_timedelta_m() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[M]"), Never
)


def test_dtype_timedelta_w() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[W]"), Never
)


def test_dtype_timedelta_d() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[D]"), Never
)


def test_dtype_timedelta_hour() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[h]"), Never
)


def test_dtype_timedelta_min() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[m]"), Never
)


def test_dtype_timedelta_mus() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[μs]"), Never
)


def test_dtype_timedelta_ps() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[ps]"), Never
)


def test_dtype_timedelta_fs() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[fs]"), Never
)


def test_dtype_timedelta_as() -> None:
if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series([datetime.timedelta(seconds=1)], dtype="timedelta64[as]"), Never
)


@pytest.mark.parametrize(
"cast_arg, target_type", ASTYPE_TIMEDELTA_ARGS.items(), ids=repr
)
Expand Down
Loading