-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
datetime.__sub__
overload order
#10924
Comments
The order in which the runtime does its type checks shouldn't really be relevant to the order of the overloads in the stubs. Can you produce a minimal, reproducible example where a type checker is inferring the wrong thing as a result of the current overload order? |
Theoretically, this would produce wrong inference if someone created an intersection type of These are more theoretical than of any real world relevance. The real reason I bring this up is that both |
We unfortunately see this quite often: overloads are only order-independent if the arguments are mutually exclusive. Since there are no intersection-types |
Even for alternative implementations such as pypy, it wouldn't be possible to create a class that is a subclass of Python 3.11.5 (tags/v3.11.5:cce6ba9, Aug 24 2023, 14:38:34) [MSC v.1936 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> # this ensures that `import datetime` imports the pure-Python version:
>>> sys.modules['_datetime'] = None
>>> import datetime
>>> datetime
<module 'datetime' from 'C:\\Users\\alexw\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\datetime.py'>
>>> class Baffling(datetime.date, datetime.timedelta): pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: multiple bases have instance lay-out conflict
>>> class Baffling(datetime.timedelta, datetime.date): pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: multiple bases have instance lay-out conflict
>>> class Baffling(datetime.timedelta, datetime.datetime): pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: multiple bases have instance lay-out conflict
>>> class Baffling(datetime.datetime, datetime.timedelta): pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: multiple bases have instance lay-out conflict |
So here is the full protocol example, based on discussion here. Full examplefrom datetime import datetime, timedelta
from typing import (
Protocol,
Self,
SupportsFloat,
SupportsInt,
TypeVar,
overload,
runtime_checkable,
)
from numpy import datetime64, float64, int64, timedelta64
from pandas import Timedelta, Timestamp
TD_Type = TypeVar("TD_Type", bound="TimeDelta")
DT_Type = TypeVar("DT_Type", bound="DateTime[TimeDelta]")
@runtime_checkable
class TimeDelta(Protocol):
"""Time delta provides several arithmetical operations."""
# unary operations
def __pos__(self: TD_Type) -> TD_Type: ...
def __neg__(self: TD_Type) -> TD_Type: ...
def __abs__(self: TD_Type) -> TD_Type: ...
def __bool__(self) -> bool: ...
# comparisons
def __le__(self: TD_Type, other: TD_Type, /) -> bool: ...
def __lt__(self: TD_Type, other: TD_Type, /) -> bool: ...
def __ge__(self: TD_Type, other: TD_Type, /) -> bool: ...
def __gt__(self: TD_Type, other: TD_Type, /) -> bool: ...
# arithmetic
# addition +
def __add__(self: TD_Type, other: TD_Type, /) -> TD_Type: ...
def __radd__(self: TD_Type, other: TD_Type, /) -> TD_Type: ...
# subtraction -
def __sub__(self: TD_Type, other: TD_Type, /) -> TD_Type: ...
def __rsub__(self: TD_Type, other: TD_Type, /) -> TD_Type: ...
# multiplication *
def __mul__(self: TD_Type, other: int, /) -> TD_Type: ...
def __rmul__(self: TD_Type, other: int, /) -> TD_Type: ...
# division /
def __truediv__(self: TD_Type, other: TD_Type, /) -> SupportsFloat: ...
# @overload
# def __truediv__(self, other: Self, /) -> float: ...
# @overload
# def __truediv__(self, other: float, /) -> Self: ...
# floor division //
def __floordiv__(self: TD_Type, other: TD_Type, /) -> SupportsInt: ...
# @overload
# def __floordiv__(self, other: Self, /) -> int: ...
# @overload
# def __floordiv__(self, other: int, /) -> Self: ...
# modulo %
def __mod__(self: TD_Type, other: TD_Type, /) -> TD_Type: ...
# NOTE: __rmod__ missing on fallback pydatetime
# def __rmod__(self, other: Self, /) -> Self: ...
# divmod
def __divmod__(self: TD_Type, other: TD_Type, /) -> tuple[SupportsInt, TD_Type]: ...
# NOTE: __rdivmod__ missing on fallback pydatetime
# def __rdivmod__(self, other: Self, /) -> tuple[SupportsInt, Self]: ...
@runtime_checkable
class DateTime(Protocol[TD_Type]): # bind appropriate TimeDelta type
"""Datetime can be compared and subtracted."""
def __le__(self: DT_Type, other: DT_Type, /) -> bool: ...
def __lt__(self: DT_Type, other: DT_Type, /) -> bool: ...
def __ge__(self: DT_Type, other: DT_Type, /) -> bool: ...
def __gt__(self: DT_Type, other: DT_Type, /) -> bool: ...
def __add__(self: DT_Type, other: TD_Type, /) -> DT_Type: ...
def __radd__(self: DT_Type, other: TD_Type, /) -> DT_Type: ...
# Fallback: no overloads
# def __sub__(self, other: Self, /) -> TD_Type: ...
# order A
@overload
def __sub__(self, other: Self, /) -> TD_Type: ...
@overload
def __sub__(self, other: TD_Type, /) -> Self: ...
# order B
# @overload
# def __sub__(self, other: TD_Type, /) -> Self: ...
# @overload
# def __sub__(self, other: Self, /) -> TD_Type: ...
# fmt: off
python_dt: DateTime[timedelta] = datetime.fromisoformat("2021-01-01") # incompatible with A
numpy_dt: DateTime[timedelta64] = datetime64("2021-01-01") # incompatible with B
pandas_dt: DateTime[Timedelta] = Timestamp("2021-01-01") # incompatible with B
# fmt: on The pickle is this: if I choose the over load order # order A
@overload
def __sub__(self, other: Self, /) -> TD_Type: ...
@overload
def __sub__(self, other: TD_Type, /) -> Self: ... for # order B
@overload
def __sub__(self, other: TD_Type, /) -> Self: ...
@overload
def __sub__(self, other: Self, /) -> TD_Type: ... It is compatible with Thus, the different overload orders make it impossible to have a nice joint protocol. Based on the fact that cpython's source code is aligned with overload order A, I think it is reasonable to request to use the corresponding overload order in the stubs. |
Btw. |
I think I've seen a few instances where pyright is slightly less fussy when it comes to protocol matching than mypy is. (I make no judgement as to which behaviour is "correct", in this case or in others.) |
In terms of the principle of the thing, I feel like typeshed is sorta upstream to numpy or pandas when it comes to typing, so I kinda feel like they should be following our lead when it comes to the order of the overloads, rather than vice versa. But, having said that... if this is causing issues, and we're the "odd one out", I'd be okay with making this change -- I can't think of any ways that this could cause regressions for other users. The current order does feel more logical to me currently -- to me, it follows a standard pattern where the more specific overloads are listed first, and the more general "catch-all" overloads follow later. In this specific case, I don't think the order matters hugely, but it would break with that standard pattern to switch the order. |
Off-topic, but I wouldn't recommend using |
To be honest the And secondly, in what sense do you mean is |
I took a look at this, and it seems huge amounts of time are spent on calling |
It sounds like you're measuring using Python 3.11 or lower; you'll find the performance profile is very different on py312+. We've already implemented the optimisations you suggest, but this does not negate any of the points I've made. |
FYI, we've also backported the performance improvements to But even with the improvements in py312+, |
Fixed in #10999 |
Currently, the overload order is:
typeshed/stdlib/datetime.pyi
Lines 319 to 323 in 40caa05
However, both the c-module and the fallback
pydatetime
first check if other is adatetime
:https://github.com/python/cpython/blob/c7d68f907ad3e3aa17546df92a32bddb145a69bf/Lib/_pydatetime.py#L2235-L2257
https://github.com/python/cpython/blob/1198076447f35b19a9173866ccb9839f3bcf3f17/Modules/_datetimemodule.c#L5608
So shouldn't the overload order be vice versa?
Also, I noticed that at runtime,
timedelta
has__rdivmod__
,__rmod__
anddatetime
has__rsub__
, which are missing in the stub. But I guess this is because the pure python fallback doesn't have them.The text was updated successfully, but these errors were encountered: