Skip to content

Commit 94d68ff

Browse files
committed
Merge branch 'main' into feature/cmp0xff/index-add
2 parents 9f7b696 + e959fc9 commit 94d68ff

File tree

12 files changed

+574
-383
lines changed

12 files changed

+574
-383
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ repos:
1111
hooks:
1212
- id: isort
1313
- repo: https://github.com/astral-sh/ruff-pre-commit
14-
rev: v0.12.3
14+
rev: v0.12.10
1515
hooks:
1616
- id: ruff-check
1717
args: [

docs/philosophy.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,43 @@ The type `TimestampSeries` is the result of creating a series from `pd.to_dateti
6161
the type `TimedeltaSeries` is the result of subtracting two `TimestampSeries` as well as
6262
the result of `pd.to_timedelta()`.
6363

64+
### Generic Series have restricted arithmetic
65+
66+
Consider the following Series from a DataFrame:
67+
68+
```python
69+
import pandas as pd
70+
from typing_extensions import reveal_type
71+
from typing import TYPE_CHECKING, cast
72+
73+
if TYPE_CHECKING:
74+
from pandas.core.series import TimestampSeries # noqa: F401
75+
76+
77+
frame = pd.DataFrame({"timestamp": [pd.Timestamp(2025, 8, 26)], "tag": ["one"], "value": [1.0]})
78+
values = frame["value"]
79+
reveal_type(values) # type checker: Series[Any], runtime: Series
80+
new_values = values + 2
81+
82+
timestamps = frame["timestamp"]
83+
reveal_type(timestamps) # type checker: Series[Any], runtime: Series
84+
reveal_type(timestamps - pd.Timestamp(2025, 7, 12)) # type checker: Unknown and error, runtime: Series
85+
reveal_type(cast("TimestampSeries", timestamps) - pd.Timestamp(2025, 7, 12)) # type checker: TimedeltaSeries, runtime: Series
86+
87+
tags = frame["tag"]
88+
reveal_type("suffix" + tags) # type checker: Never, runtime: Series
89+
```
90+
91+
Since they are taken from a DataFrame, all three of them, `values`, `timestamps`
92+
and `tags`, are recognized by type checkers as `Series[Any]`. The code snippet
93+
runs fine at runtime. In the stub for type checking, however, we restrict
94+
generic Series to perform arithmetic operations only with numeric types, and
95+
give `Series[Any]` for the results. For `Timedelta`, `Timestamp`, `str`, etc.,
96+
arithmetic is restricted to `Series[Any]` and the result is either undefined,
97+
showing `Unknown` and errors, or `Never`. Users are encouraged to cast such
98+
generic Series to ones with concrete types, so that type checkers can provide
99+
meaningful results.
100+
64101
### Interval is Generic
65102

66103
A pandas `Interval` can be a time interval, an interval of integers, or an interval of

pandas-stubs/_version.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ from typing import Literal
22

33
version_json: str = ...
44

5-
_stub_version: Literal["2.3.0.250703"]
5+
_stub_version: Literal["2.3.2.250827"]

pandas-stubs/core/series.pyi

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ from pandas._typing import (
187187
np_ndarray_anyint,
188188
np_ndarray_bool,
189189
np_ndarray_complex,
190-
np_ndarray_dt,
191190
np_ndarray_float,
192191
np_ndarray_str,
193192
np_ndarray_td,
@@ -259,9 +258,20 @@ class _LocIndexerSeries(_LocIndexer, Generic[S1]):
259258
value: S1 | ArrayLike | Series[S1] | None,
260259
) -> None: ...
261260

262-
_ListLike: TypeAlias = (
261+
_ListLike: TypeAlias = ArrayLike | dict[_str, np.ndarray] | SequenceNotStr[S1]
262+
_ListLikeS1: TypeAlias = (
263263
ArrayLike | dict[_str, np.ndarray] | Sequence[S1] | IndexOpsMixin[S1]
264264
)
265+
_NumListLike: TypeAlias = (
266+
ExtensionArray
267+
| np_ndarray_bool
268+
| np_ndarray_anyint
269+
| np_ndarray_float
270+
| np_ndarray_complex
271+
| dict[_str, np.ndarray]
272+
| Sequence[complex]
273+
| IndexOpsMixin[complex]
274+
)
265275

266276
class Series(IndexOpsMixin[S1], NDFrame):
267277
# Define __index__ because mypy thinks Series follows protocol `SupportsIndex` https://github.com/pandas-dev/pandas-stubs/pull/1332#discussion_r2285648790
@@ -417,7 +427,9 @@ class Series(IndexOpsMixin[S1], NDFrame):
417427
@overload
418428
def __new__(
419429
cls,
420-
data: S1 | _ListLike[S1] | dict[HashableT1, S1] | KeysView[S1] | ValuesView[S1],
430+
data: (
431+
S1 | _ListLikeS1[S1] | dict[HashableT1, S1] | KeysView[S1] | ValuesView[S1]
432+
),
421433
index: AxesData | None = ...,
422434
dtype: Dtype = ...,
423435
name: Hashable = ...,
@@ -1617,7 +1629,9 @@ class Series(IndexOpsMixin[S1], NDFrame):
16171629
# just failed to generate these so I couldn't match
16181630
# them up.
16191631
@overload
1620-
def __add__(self: Series[Never], other: Scalar | _ListLike | Series) -> Series: ...
1632+
def __add__(self: Series[Never], other: _str) -> Never: ...
1633+
@overload
1634+
def __add__(self: Series[Never], other: complex | _ListLike | Series) -> Series: ...
16211635
@overload
16221636
def __add__(self, other: Series[Never]) -> Series: ...
16231637
@overload
@@ -1695,7 +1709,15 @@ class Series(IndexOpsMixin[S1], NDFrame):
16951709
@overload
16961710
def add(
16971711
self: Series[Never],
1698-
other: Scalar | _ListLike | Series,
1712+
other: _str,
1713+
level: Level | None = None,
1714+
fill_value: float | None = None,
1715+
axis: int = 0,
1716+
) -> Never: ...
1717+
@overload
1718+
def add(
1719+
self: Series[Never],
1720+
other: complex | _ListLike | Series,
16991721
level: Level | None = None,
17001722
fill_value: float | None = None,
17011723
axis: int = 0,
@@ -1838,7 +1860,11 @@ class Series(IndexOpsMixin[S1], NDFrame):
18381860
axis: int = 0,
18391861
) -> Series[_str]: ...
18401862
@overload # type: ignore[override]
1841-
def __radd__(self: Series[Never], other: Scalar | _ListLike) -> Series: ...
1863+
def __radd__(self: Series[Never], other: _str) -> Never: ...
1864+
@overload
1865+
def __radd__(
1866+
self: Series[Never], other: complex | _ListLike | Series
1867+
) -> Series: ...
18421868
@overload
18431869
def __radd__(
18441870
self: Series[bool],
@@ -1910,7 +1936,23 @@ class Series(IndexOpsMixin[S1], NDFrame):
19101936
@overload
19111937
def radd(
19121938
self: Series[Never],
1913-
other: Scalar | _ListLike | Series,
1939+
other: _str,
1940+
level: Level | None = None,
1941+
fill_value: float | None = None,
1942+
axis: int = 0,
1943+
) -> Never: ...
1944+
@overload
1945+
def radd(
1946+
self: Series[Never],
1947+
other: complex | _ListLike | Series,
1948+
level: Level | None = None,
1949+
fill_value: float | None = None,
1950+
axis: int = 0,
1951+
) -> Series: ...
1952+
@overload
1953+
def radd(
1954+
self: Series[S1],
1955+
other: Series[Never],
19141956
level: Level | None = None,
19151957
fill_value: float | None = None,
19161958
axis: int = 0,
@@ -2049,7 +2091,9 @@ class Series(IndexOpsMixin[S1], NDFrame):
20492091
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta | date
20502092
) -> Series[_bool]: ...
20512093
@overload
2052-
def __mul__(self: Series[Never], other: complex | _ListLike | Series) -> Series: ...
2094+
def __mul__(
2095+
self: Series[Never], other: complex | _NumListLike | Series
2096+
) -> Series: ...
20532097
@overload
20542098
def __mul__(self, other: Series[Never]) -> Series: ... # type: ignore[overload-overlap]
20552099
@overload
@@ -2244,7 +2288,7 @@ class Series(IndexOpsMixin[S1], NDFrame):
22442288
) -> TimedeltaSeries: ...
22452289
@overload
22462290
def __rmul__(
2247-
self: Series[Never], other: complex | _ListLike | Series
2291+
self: Series[Never], other: complex | _NumListLike | Series
22482292
) -> Series: ...
22492293
@overload
22502294
def __rmul__(self, other: Series[Never]) -> Series: ... # type: ignore[overload-overlap]
@@ -2473,12 +2517,11 @@ class Series(IndexOpsMixin[S1], NDFrame):
24732517
@overload
24742518
def __rxor__(self, other: int | np_ndarray_anyint | Series[int]) -> Series[int]: ...
24752519
@overload
2476-
def __sub__(
2477-
self: Series[Never],
2478-
other: datetime | np.datetime64 | np_ndarray_dt | TimestampSeries,
2479-
) -> TimedeltaSeries: ...
2520+
def __sub__(self: Series[Never], other: TimestampSeries) -> Never: ...
24802521
@overload
2481-
def __sub__(self: Series[Never], other: complex | _ListLike | Series) -> Series: ...
2522+
def __sub__(
2523+
self: Series[Never], other: complex | _NumListLike | Series
2524+
) -> Series: ...
24822525
@overload
24832526
def __sub__(self, other: Series[Never]) -> Series: ... # type: ignore[overload-overlap]
24842527
@overload
@@ -2569,15 +2612,15 @@ class Series(IndexOpsMixin[S1], NDFrame):
25692612
@overload
25702613
def sub(
25712614
self: Series[Never],
2572-
other: datetime | np.datetime64 | np_ndarray_dt | TimestampSeries,
2615+
other: TimestampSeries,
25732616
level: Level | None = None,
25742617
fill_value: float | None = None,
25752618
axis: int = 0,
2576-
) -> TimedeltaSeries: ...
2619+
) -> Never: ...
25772620
@overload
25782621
def sub(
25792622
self: Series[Never],
2580-
other: complex | _ListLike | Series,
2623+
other: complex | _NumListLike | Series,
25812624
level: Level | None = None,
25822625
fill_value: float | None = None,
25832626
axis: int = 0,
@@ -2703,13 +2746,10 @@ class Series(IndexOpsMixin[S1], NDFrame):
27032746
axis: int = 0,
27042747
) -> TimedeltaSeries: ...
27052748
@overload
2706-
def __rsub__( # type: ignore[misc]
2707-
self: Series[Never],
2708-
other: datetime | np.datetime64 | np_ndarray_dt | TimestampSeries,
2709-
) -> TimedeltaSeries: ...
2749+
def __rsub__(self: Series[Never], other: TimestampSeries) -> Never: ... # type: ignore[misc]
27102750
@overload
27112751
def __rsub__(
2712-
self: Series[Never], other: complex | _ListLike | Series
2752+
self: Series[Never], other: complex | _NumListLike | Series
27132753
) -> Series: ...
27142754
@overload
27152755
def __rsub__(self, other: Series[Never]) -> Series: ...
@@ -2779,15 +2819,15 @@ class Series(IndexOpsMixin[S1], NDFrame):
27792819
@overload
27802820
def rsub(
27812821
self: Series[Never],
2782-
other: datetime | np.datetime64 | np_ndarray_dt | TimestampSeries,
2822+
other: TimestampSeries,
27832823
level: Level | None = None,
27842824
fill_value: float | None = None,
27852825
axis: int = 0,
2786-
) -> TimedeltaSeries: ...
2826+
) -> Never: ...
27872827
@overload
27882828
def rsub(
27892829
self: Series[Never],
2790-
other: complex | _ListLike | Series,
2830+
other: complex | _NumListLike | Series,
27912831
level: Level | None = None,
27922832
fill_value: float | None = None,
27932833
axis: int = 0,
@@ -2885,8 +2925,8 @@ class Series(IndexOpsMixin[S1], NDFrame):
28852925
axis: int = 0,
28862926
) -> Series[complex]: ...
28872927
@overload
2888-
def __truediv__(
2889-
self: Series[Never], other: complex | _ListLike | Series
2928+
def __truediv__( # type:ignore[overload-overlap]
2929+
self: Series[Never], other: complex | _NumListLike | Series
28902930
) -> Series: ...
28912931
@overload
28922932
def __truediv__(self, other: Series[Never]) -> Series: ...
@@ -3081,8 +3121,8 @@ class Series(IndexOpsMixin[S1], NDFrame):
30813121
) -> Series: ...
30823122
div = truediv
30833123
@overload
3084-
def __rtruediv__(
3085-
self: Series[Never], other: complex | _ListLike | Series
3124+
def __rtruediv__( # type:ignore[overload-overlap]
3125+
self: Series[Never], other: complex | _NumListLike | Series
30863126
) -> Series: ...
30873127
@overload
30883128
def __rtruediv__(self, other: Series[Never]) -> Series: ...

pyproject.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pandas-stubs"
3-
version = "2.3.0.250703"
3+
version = "2.3.2.250827"
44
description = "Type annotations for pandas"
55
authors = ["The Pandas Development Team <[email protected]>"]
66
license = "BSD-3-Clause"
@@ -23,7 +23,8 @@ classifiers = [
2323
"Topic :: Scientific/Engineering",
2424
"Typing :: Stubs Only",
2525
]
26-
packages = [{ "include" = "pandas-stubs" }]
26+
packages = [{ include = "pandas-stubs" }]
27+
exclude = [ "pandas-stubs/__init__.py" ]
2728

2829
[tool.poetry.urls]
2930
"Bug Tracker" = "https://github.com/pandas-dev/pandas-stubs/issues"
@@ -35,12 +36,12 @@ types-pytz = ">= 2022.1.1"
3536
numpy = ">= 1.23.5"
3637

3738
[tool.poetry.group.dev.dependencies]
38-
mypy = "1.17.0"
39-
pandas = "2.3.1"
39+
mypy = "1.17.1"
40+
pandas = "2.3.2"
4041
pyarrow = ">=10.0.1"
4142
pytest = ">=7.1.2"
4243
pyright = ">=1.1.404"
43-
ty = "^0.0.1a8"
44+
ty = "^0.0.1a9"
4445
pyrefly = "^0.21.0"
4546
poethepoet = ">=0.16.5"
4647
loguru = ">=0.6.0"

tests/series/arithmetic/str/test_add.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919

2020
def test_add_py_scalar() -> None:
21-
"""Testpd.Series[str]+ Python native 'scalar's"""
21+
"""Test pd.Series[str] + Python native 'scalar's"""
2222
i = 4
2323
r0 = "right"
2424

@@ -35,12 +35,12 @@ def test_add_py_scalar() -> None:
3535
check(assert_type(left.add(r0), "pd.Series[str]"), pd.Series, str)
3636

3737
if TYPE_CHECKING_INVALID_USAGE:
38-
left.radd(i) # type: ignore[call-overload] # pyright: ignore[reportArgumentType]
38+
left.radd(i) # type: ignore[call-overload] # pyright: ignore[reportArgumentType, reportCallIssue]
3939
check(assert_type(left.radd(r0), "pd.Series[str]"), pd.Series, str)
4040

4141

4242
def test_add_py_sequence() -> None:
43-
"""Testpd.Series[str]+ Python native sequence"""
43+
"""Test pd.Series[str] + Python native sequence"""
4444
i = [3, 5, 8]
4545
r0 = ["a", "bc", "def"]
4646
r1 = tuple(r0)
@@ -61,13 +61,13 @@ def test_add_py_sequence() -> None:
6161
check(assert_type(left.add(r1), "pd.Series[str]"), pd.Series, str)
6262

6363
if TYPE_CHECKING_INVALID_USAGE:
64-
left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
64+
left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType,reportCallIssue]
6565
check(assert_type(left.radd(r0), "pd.Series[str]"), pd.Series, str)
6666
check(assert_type(left.radd(r1), "pd.Series[str]"), pd.Series, str)
6767

6868

6969
def test_add_numpy_array() -> None:
70-
"""Testpd.Series[str]+ numpy array"""
70+
"""Test pd.Series[str] + numpy array"""
7171
i = np.array([3, 5, 8], np.int64)
7272
r0 = np.array(["a", "bc", "def"], np.str_)
7373

@@ -96,12 +96,12 @@ def test_add_numpy_array() -> None:
9696
check(assert_type(left.add(r0), "pd.Series[str]"), pd.Series, str)
9797

9898
if TYPE_CHECKING_INVALID_USAGE:
99-
left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
99+
left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType, reportCallIssue]
100100
check(assert_type(left.radd(r0), "pd.Series[str]"), pd.Series, str)
101101

102102

103103
def test_add_pd_series() -> None:
104-
"""Testpd.Series[str]+ pandas series"""
104+
"""Test pd.Series[str] + pandas series"""
105105
i = pd.Series([3, 5, 8])
106106
r0 = pd.Series(["a", "bc", "def"])
107107

@@ -118,5 +118,5 @@ def test_add_pd_series() -> None:
118118
check(assert_type(left.add(r0), "pd.Series[str]"), pd.Series, str)
119119

120120
if TYPE_CHECKING_INVALID_USAGE:
121-
left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
121+
left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType, reportCallIssue]
122122
check(assert_type(left.radd(r0), "pd.Series[str]"), pd.Series, str)

0 commit comments

Comments
 (0)