Skip to content

Commit 5768214

Browse files
authored
refactor(series)!: 🕰️ drop TimestampSeries (#1274)
* refactor: only drop TimestampSeries #1273 (review) * fix(comment): https://github.com/pandas-dev/pandas-stubs/pull/1274/files#r2229555145 * less mypy ignore * fix: even less mypy ignores * refactor(comment): https://github.com/pandas-dev/pandas-stubs/pull/1274/files#r2229550572 * feat: sub * fix(comment): https://github.com/pandas-dev/pandas-stubs/pull/1274/files#r2229581983 * fix: pyrefly * fix(comment): https://github.com/pandas-dev/pandas-stubs/pull/1274/files#r2229580369 * fix(comment): #1274 (comment) * fix(comment): #1274 (comment) * fix(comment): #1274 (comment) * fix: reduce ignore * refactor: explain a temporary failure * fix(comment): #1274 (comment) * feat: arithmetic sub * fix(comment): #1312 (comment) * feat(comment): #1312 (comment) * fix(comment): completeness #1312 (review) * feat(series): arithmetic mul * fix: type asserts * feat: tests for addition * fix(mypy): attempt for python > 310 * feat: tests for subtraction * fix: pyright * fix(comment): #1274 (comment) * refactor: revert unused design * feat: progressive philosophy * fix(pytest): remove invalid tests * fix: comment * fix(comment): https://github.com/pandas-dev/pandas-stubs/pull/1274/files/f855f8655fcbc0c13879f404926034621641bc58#r2291133305 * fix(comment): #1274 (comment) * chore(philosophy): first draft * fix(pyright): median * fix: comment * fix(comment): #1274 (review) * fix(comment): test_sub * fix: typo
1 parent c38b6a3 commit 5768214

22 files changed

+1274
-486
lines changed

docs/philosophy.md

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,67 +36,70 @@ This also allows type checking for operations on series that contain date/time d
3636
the following example that creates two series of datetimes with corresponding arithmetic.
3737

3838
```python
39+
import pandas as pd
40+
from typing import reveal_type
41+
3942
s1 = pd.Series(pd.to_datetime(["2022-05-01", "2022-06-01"]))
4043
reveal_type(s1)
4144
s2 = pd.Series(pd.to_datetime(["2022-05-15", "2022-06-15"]))
4245
reveal_type(s2)
4346
td = s1 - s2
4447
reveal_type(td)
4548
ssum = s1 + s2
46-
reveal_type(ssum)
4749
```
4850

49-
The above code (without the `reveal_type()` statements) will raise an `Exception` on the computation of `ssum` because it is
51+
The above code (without the `reveal_type()` statements) will get an error on the computation of `ssum` because it is
5052
inappropriate to add two series containing `Timestamp` values. The types will be
51-
revealed as follows:
53+
revealed by `mypy` as follows:
5254

5355
```text
54-
ttest.py:4: note: Revealed type is "pandas.core.series.TimestampSeries"
55-
ttest.py:6: note: Revealed type is "pandas.core.series.TimestampSeries"
56-
ttest.py:8: note: Revealed type is "pandas.core.series.TimedeltaSeries"
57-
ttest.py:10: note: Revealed type is "builtins.Exception"
56+
ttest.py:5: note: Revealed type is "pandas.core.series.Series[pandas._libs.tslibs.timestamps.Timestamp]"
57+
ttest.py:7: note: Revealed type is "pandas.core.series.Series[pandas._libs.tslibs.timestamps.Timestamp]"
58+
ttest.py:9: note: Revealed type is "pandas.core.series.TimedeltaSeries"
59+
ttest.py:10: error: Unsupported operand types for + ("Series[Timestamp]" and "Series[Timestamp]") [operator]
5860
```
5961

60-
The type `TimestampSeries` is the result of creating a series from `pd.to_datetime()`, while
61-
the type `TimedeltaSeries` is the result of subtracting two `TimestampSeries` as well as
62+
The type `Series[Timestamp]` is the result of creating a series from `pd.to_datetime()`, while
63+
the type `TimedeltaSeries` is the result of subtracting two `Series[Timestamp]` as well as
6264
the result of `pd.to_timedelta()`.
6365

64-
### Generic Series have restricted arithmetic
66+
### Progressive arithmetic typing for generic Series
6567

6668
Consider the following Series from a DataFrame:
6769

6870
```python
6971
import pandas as pd
7072
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
7573

7674

77-
frame = pd.DataFrame({"timestamp": [pd.Timestamp(2025, 8, 26)], "tag": ["one"], "value": [1.0]})
75+
frame = pd.DataFrame({"timestamp": [pd.Timestamp(2025, 9, 15)], "tag": ["one"], "value": [1.0]})
7876
values = frame["value"]
7977
reveal_type(values) # type checker: Series[Any], runtime: Series
8078
new_values = values + 2
8179

8280
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
81+
reveal_type(timestamps - pd.Timestamp(2025, 7, 12)) # type checker: TimedeltaSeries, runtime: Series
8682

8783
tags = frame["tag"]
88-
reveal_type("suffix" + tags) # type checker: Never, runtime: Series
84+
reveal_type("suffix" + tags) # type checker: Series[str], runtime: Series
8985
```
9086

91-
Since they are taken from a DataFrame, all three of them, `values`, `timestamps`
87+
Since these Series are taken from a DataFrame, all three of them, `values`, `timestamps`
9288
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.
89+
runs fine at runtime. In the stub for type checking, when there is only one
90+
valid outcome, we provide the typing of this outcome as the result. For
91+
example, if a `Timestamp` is subtracted from a `Series[Any]`, or a `str`
92+
is added to a `Series[Any]`, valid outcomes can only be `TimedeltaSeries` and
93+
`Series[str]`, respectively, which will be realized when the left operands
94+
are actually `Series[Timestamp]` and `Series[str]`, respectively.
95+
96+
Note that static type checkers cannot determine the contents of a `Series[Any]`
97+
at runtime. Users are invited to verify the results provided progressivly by the
98+
type checkers and be warned if they are unreasonable.
99+
100+
When there are several possible valid outcomes of an arithmetic expression,
101+
for example numeric types `Series[bool]`, `Series[int]`, etc., `Series[Any]`
102+
will be given as the resulting type of the arithmetic operation.
100103

101104
### Interval is Generic
102105

pandas-stubs/_libs/interval.pyi

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ from pandas import (
1313
Timedelta,
1414
Timestamp,
1515
)
16-
from pandas.core.series import (
17-
TimedeltaSeries,
18-
TimestampSeries,
19-
)
16+
from pandas.core.series import TimedeltaSeries
2017

2118
from pandas._typing import (
2219
IntervalClosedType,
@@ -176,7 +173,7 @@ class Interval(IntervalMixin, Generic[_OrderableT]):
176173
@overload
177174
def __gt__(
178175
self,
179-
other: Series[int] | Series[float] | TimestampSeries | TimedeltaSeries,
176+
other: Series[int] | Series[float] | Series[Timestamp] | TimedeltaSeries,
180177
) -> Series[bool]: ...
181178
@overload
182179
def __lt__(self, other: Interval[_OrderableT]) -> bool: ...
@@ -187,7 +184,7 @@ class Interval(IntervalMixin, Generic[_OrderableT]):
187184
@overload
188185
def __lt__(
189186
self,
190-
other: Series[int] | Series[float] | TimestampSeries | TimedeltaSeries,
187+
other: Series[int] | Series[float] | Series[Timestamp] | TimedeltaSeries,
191188
) -> Series[bool]: ...
192189
@overload
193190
def __ge__(self, other: Interval[_OrderableT]) -> bool: ...
@@ -198,7 +195,7 @@ class Interval(IntervalMixin, Generic[_OrderableT]):
198195
@overload
199196
def __ge__(
200197
self,
201-
other: Series[int] | Series[float] | TimestampSeries | TimedeltaSeries,
198+
other: Series[int] | Series[float] | Series[Timestamp] | TimedeltaSeries,
202199
) -> Series[bool]: ...
203200
@overload
204201
def __le__(self, other: Interval[_OrderableT]) -> bool: ...

pandas-stubs/_libs/tslibs/timedeltas.pyi

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ from pandas import (
1717
Series,
1818
TimedeltaIndex,
1919
)
20-
from pandas.core.series import (
21-
TimedeltaSeries,
22-
TimestampSeries,
23-
)
20+
from pandas.core.series import TimedeltaSeries
2421
from typing_extensions import (
2522
Self,
2623
TypeAlias,
@@ -170,7 +167,7 @@ class Timedelta(timedelta):
170167
other: TimedeltaSeries,
171168
) -> TimedeltaSeries: ...
172169
@overload
173-
def __add__(self, other: TimestampSeries) -> TimestampSeries: ...
170+
def __add__(self, other: Series[Timestamp]) -> Series[Timestamp]: ...
174171
@overload
175172
def __radd__(self, other: np.datetime64) -> Timestamp: ...
176173
@overload

pandas-stubs/_libs/tslibs/timestamps.pyi

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ from pandas.core.indexes.base import Index
2525
from pandas.core.series import (
2626
Series,
2727
TimedeltaSeries,
28-
TimestampSeries,
2928
)
3029
from typing_extensions import (
3130
Never,
@@ -187,7 +186,7 @@ class Timestamp(datetime, SupportsIndex):
187186
self, other: np_ndarray[ShapeT, np.datetime64]
188187
) -> np_ndarray[ShapeT, np.bool]: ...
189188
@overload
190-
def __le__(self, other: TimestampSeries) -> Series[bool]: ...
189+
def __le__(self, other: Series[Timestamp]) -> Series[bool]: ...
191190
@overload # type: ignore[override]
192191
def __lt__(self, other: Timestamp | datetime | np.datetime64) -> bool: ... # type: ignore[misc]
193192
@overload
@@ -197,7 +196,7 @@ class Timestamp(datetime, SupportsIndex):
197196
self, other: np_ndarray[ShapeT, np.datetime64]
198197
) -> np_ndarray[ShapeT, np.bool]: ...
199198
@overload
200-
def __lt__(self, other: TimestampSeries) -> Series[bool]: ...
199+
def __lt__(self, other: Series[Timestamp]) -> Series[bool]: ...
201200
@overload # type: ignore[override]
202201
def __ge__(self, other: Timestamp | datetime | np.datetime64) -> bool: ... # type: ignore[misc]
203202
@overload
@@ -207,7 +206,7 @@ class Timestamp(datetime, SupportsIndex):
207206
self, other: np_ndarray[ShapeT, np.datetime64]
208207
) -> np_ndarray[ShapeT, np.bool]: ...
209208
@overload
210-
def __ge__(self, other: TimestampSeries) -> Series[bool]: ...
209+
def __ge__(self, other: Series[Timestamp]) -> Series[bool]: ...
211210
@overload # type: ignore[override]
212211
def __gt__(self, other: Timestamp | datetime | np.datetime64) -> bool: ... # type: ignore[misc]
213212
@overload
@@ -217,7 +216,7 @@ class Timestamp(datetime, SupportsIndex):
217216
self, other: np_ndarray[ShapeT, np.datetime64]
218217
) -> np_ndarray[ShapeT, np.bool]: ...
219218
@overload
220-
def __gt__(self, other: TimestampSeries) -> Series[bool]: ...
219+
def __gt__(self, other: Series[Timestamp]) -> Series[bool]: ...
221220
# error: Signature of "__add__" incompatible with supertype "date"/"datetime"
222221
@overload # type: ignore[override]
223222
def __add__(
@@ -226,7 +225,7 @@ class Timestamp(datetime, SupportsIndex):
226225
@overload
227226
def __add__(self, other: timedelta | np.timedelta64 | Tick) -> Self: ...
228227
@overload
229-
def __add__(self, other: TimedeltaSeries) -> TimestampSeries: ...
228+
def __add__(self, other: TimedeltaSeries) -> Series[Timestamp]: ...
230229
@overload
231230
def __add__(self, other: TimedeltaIndex) -> DatetimeIndex: ...
232231
@overload
@@ -239,23 +238,21 @@ class Timestamp(datetime, SupportsIndex):
239238
) -> np_ndarray[ShapeT, np.datetime64]: ...
240239
# TODO: test dt64
241240
@overload # type: ignore[override]
242-
def __sub__(self, other: Timestamp | datetime | np.datetime64) -> Timedelta: ...
241+
def __sub__(self, other: datetime | np.datetime64) -> Timedelta: ...
243242
@overload
244243
def __sub__(self, other: timedelta | np.timedelta64 | Tick) -> Self: ...
245244
@overload
246245
def __sub__(self, other: TimedeltaIndex) -> DatetimeIndex: ...
247246
@overload
248-
def __sub__(self, other: TimedeltaSeries) -> TimestampSeries: ...
249-
@overload
250-
def __sub__(self, other: TimestampSeries) -> TimedeltaSeries: ...
247+
def __sub__(self, other: TimedeltaSeries) -> Series[Timestamp]: ...
251248
@overload
252249
def __sub__(
253250
self, other: np_ndarray[ShapeT, np.timedelta64]
254251
) -> np_ndarray[ShapeT, np.datetime64]: ...
255252
@overload
256253
def __eq__(self, other: Timestamp | datetime | np.datetime64) -> bool: ... # type: ignore[overload-overlap] # pyright: ignore[reportOverlappingOverload]
257254
@overload
258-
def __eq__(self, other: TimestampSeries) -> Series[bool]: ... # type: ignore[overload-overlap]
255+
def __eq__(self, other: Series[Timestamp]) -> Series[bool]: ... # type: ignore[overload-overlap]
259256
@overload
260257
def __eq__(self, other: Index) -> np_1darray[np.bool]: ... # type: ignore[overload-overlap]
261258
@overload
@@ -265,7 +262,7 @@ class Timestamp(datetime, SupportsIndex):
265262
@overload
266263
def __ne__(self, other: Timestamp | datetime | np.datetime64) -> bool: ... # type: ignore[overload-overlap] # pyright: ignore[reportOverlappingOverload]
267264
@overload
268-
def __ne__(self, other: TimestampSeries) -> Series[bool]: ... # type: ignore[overload-overlap]
265+
def __ne__(self, other: Series[Timestamp]) -> Series[bool]: ... # type: ignore[overload-overlap]
269266
@overload
270267
def __ne__(self, other: Index) -> np_1darray[np.bool]: ... # type: ignore[overload-overlap]
271268
@overload

0 commit comments

Comments
 (0)