You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Incorrect type inference with __radd__ with subclass of tuple[int, ...] #19006
mypy generally correctly prioritizes __radd__ if the right operand is a subtype of the left operand. However, I discovered that it can fail to do so when creating a subclass of tuple[int, ...].
Ohh, this is interesting! tuple.__{r,}add__ methods are special-cased to handle their sizes and non-homogeneous item types. There must be something missing in fallback handling here:
In both cases, mapped = map_instance_to_supertype(left, right.partial_fallback.type) yields tuple[int, ...] and then is_erased_instance(mapped) returns False.
That's correct: only tuple[Any, ...] is a subtype of all tuples including fixed-sizes one. tuple[int, ...] is not a subtype of tuple[int, int], for example (while the reverse is true - a fixed-size tuple[int, int] is a subtype of variadic tuple[int, ...]). This Any special-case is intentional (because variadic Any tuples can arise from some untyped constructs), but it does not generalize to other tuples. https://mypy-play.net/?mypy=master&python=3.12&flags=strict&gist=d60b71630f447223ac70c69f8b7d7d62
Sorry, I linked to the wrong place - that one is for cases without __radd__, so with custom __radd__ we fall back to plain self.check_op. Runtime dispatch doesn't care about tuple params, so we ignore __radd__ when we shouldn't. Maybe just allow variadic tuples explicitly when gathering candidates in check_op_reversible (or one more flag for is_subtype)?
changed the title [-]Incorrect type inference with `__radd__` of tuple subtype against literal tuples.[/-][+]Incorrect type inference with `__radd__` with subclass of `tuple[int, ...]`[/+]on May 2, 2025
# When we do "A() + B()" where B is a subclass of A, we'll actually try calling
# B's __radd__ method first, but ONLY if B explicitly defines or overrides the
# __radd__ method.
In the specific case here, the LHS is a fixed size tuple tuple[*Ts], whereas the RHS is a variable size tuple tuple[T, ...].
So mypy checks whether is_subtype(tuple[T, ...], tuple[*Ts]); in our specific case is_subtype(tuple[int, ...], tuple[()]), which obviously fails.
However, wouldn't it make sense here to consider reinterpreting the fixed size tuple as a variable size tuple?
One could ask: Does there exist a choice of S for which tuple[*Ts] <: tuple[S, ...] and tuple[T, ...] <: tuple[S, ...]? Indeed, this is trivially true for S = object, and the minimal type that satisfies these constraints is S = T | Union[*Ts], which in our case would always be S = int, so everything checks out.
@randolf-scholz Not sure I follow, are you trying to convince me that variadic tuples should be considered subtypes of some fixed-length tuples? tuple[int, ...] might, for example, be a 3-tuple (1, 2, 3), so it cannot be a subtype of tuple[int, int] nor tuple[()]. Looking from set-oriented type theory side, no amount of upcasting and reinterpreting can make a variadic tuple subtype of a fixed tuple, the former has more elements so no bijection can exist.
By definition, tuple[T, ...] is a tuple containing zero or more elements of type T - the length is deliberately unknown. Variadic tuples are essentially sequences that happen to have class tuple, while fixed tuples are most close to product types.
If I'm not misreading, you're saying that (for some T and U) T <: U if T and U have a common supertype? That sounds wrong, consider
classA: ...
classB(A): ...
classC(A): ...
Applying your argument, does there exist a choice of S such that B <: S and C <: S? Yes, that's object and also that's A, so B <: C?
Not sure I follow, are you trying to convince me that variadic tuples should be considered subtypes of some fixed-length tuples?
That was definitely not the goal 😅. The issue is that the is_subtype check is likely too strong to be compatible with runtime, so I was wondering if one could weaken this check - in the specific case of checking reverse-op applicability - in one way or another without breaking things.
Looking at the python C-source, the actual check performed is PyType_IsSubtype(Py_TYPE(right), Py_TYPE(left)))1, which simply checks whether type(right) in type(left).__mro__2.
This check ignores generics, hence the suggestion above was to slightly weaken the is_subtype check3 to consider upcasting the parametrization of a generic type (e.g. from tuple[*Ts] to tuple[T | Union[*Ts], ...] in this case.) I do not think it ever makes sense to upcast the base type itself (e.g. from tuple to object.)
Ough, sorry, I was missing the third footnote in your previous comment - I thought you're trying to change is_subtype behaviour for all purposes.
Yes, at runtime we have essentially an issubclass check, and it really doesn't play well with static generics. Perhaps we should replace is_subtype with covers_at_runtime to determine whether __rOP__ should be called first?
Activity
sterliakov commentedon Apr 30, 2025
Ohh, this is interesting!
tuple.__{r,}add__methods are special-cased to handle their sizes and non-homogeneous item types. There must be something missing in fallback handling here:mypy/mypy/checkexpr.py
Lines 3487 to 3516 in c724a6a
randolf-scholz commentedon May 1, 2025
@sterliakov I stepped through this example with the debugger:
The last two examples fail inside this branch:
mypy/mypy/subtypes.py
Lines 472 to 494 in c724a6a
In both cases,
mapped = map_instance_to_supertype(left, right.partial_fallback.type)yieldstuple[int, ...]and thenis_erased_instance(mapped)returnsFalse.sterliakov commentedon May 1, 2025
That's correct: only
tuple[Any, ...]is a subtype of all tuples including fixed-sizes one.tuple[int, ...]is not a subtype oftuple[int, int], for example (while the reverse is true - a fixed-sizetuple[int, int]is a subtype of variadictuple[int, ...]). This Any special-case is intentional (because variadic Any tuples can arise from some untyped constructs), but it does not generalize to other tuples. https://mypy-play.net/?mypy=master&python=3.12&flags=strict&gist=d60b71630f447223ac70c69f8b7d7d62Sorry, I linked to the wrong place - that one is for cases without
__radd__, so with custom__radd__we fall back to plainself.check_op. Runtime dispatch doesn't care about tuple params, so we ignore__radd__when we shouldn't. Maybe just allow variadic tuples explicitly when gathering candidates incheck_op_reversible(or one more flag foris_subtype)?randolf-scholz commentedon May 2, 2025
Right, so it doesn't really have anything to do with tuple literals after all, I will update the OP.
[-]Incorrect type inference with `__radd__` of tuple subtype against literal tuples.[/-][+]Incorrect type inference with `__radd__` with subclass of `tuple[int, ...]`[/+]randolf-scholz commentedon May 4, 2025
@sterliakov To recapitulate:
mypy/mypy/checkexpr.py
Lines 4050 to 4062 in d68ea35
In the specific case here, the LHS is a fixed size tuple
tuple[*Ts], whereas the RHS is a variable size tupletuple[T, ...].So
mypychecks whetheris_subtype(tuple[T, ...], tuple[*Ts]); in our specific caseis_subtype(tuple[int, ...], tuple[()]), which obviously fails.However, wouldn't it make sense here to consider reinterpreting the fixed size tuple as a variable size tuple?
One could ask: Does there exist a choice of
Sfor whichtuple[*Ts] <: tuple[S, ...]andtuple[T, ...] <: tuple[S, ...]? Indeed, this is trivially true forS = object, and the minimal type that satisfies these constraints isS = T | Union[*Ts], which in our case would always beS = int, so everything checks out.sterliakov commentedon May 4, 2025
@randolf-scholz Not sure I follow, are you trying to convince me that variadic tuples should be considered subtypes of some fixed-length tuples?
tuple[int, ...]might, for example, be a 3-tuple(1, 2, 3), so it cannot be a subtype oftuple[int, int]nortuple[()]. Looking from set-oriented type theory side, no amount of upcasting and reinterpreting can make a variadic tuple subtype of a fixed tuple, the former has more elements so no bijection can exist.By definition,
tuple[T, ...]is a tuple containing zero or more elements of typeT- the length is deliberately unknown. Variadic tuples are essentially sequences that happen to have classtuple, while fixed tuples are most close to product types.If I'm not misreading, you're saying that (for some
TandU)T <: UifTandUhave a common supertype? That sounds wrong, considerApplying your argument, does there exist a choice of
Ssuch thatB <: SandC <: S? Yes, that'sobjectand also that'sA, soB <: C?Here's a link to the spec chapter laying out tuple assignability rules: https://typing.python.org/en/latest/spec/tuples.html#type-compatibility-rules
randolf-scholz commentedon May 5, 2025
That was definitely not the goal 😅. The issue is that the
is_subtypecheck is likely too strong to be compatible with runtime, so I was wondering if one could weaken this check - in the specific case of checking reverse-op applicability - in one way or another without breaking things.Looking at the python C-source, the actual check performed is
PyType_IsSubtype(Py_TYPE(right), Py_TYPE(left)))1, which simply checks whethertype(right) in type(left).__mro__2.This check ignores generics, hence the suggestion above was to slightly weaken the
is_subtypecheck3 to consider upcasting the parametrization of a generic type (e.g. fromtuple[*Ts]totuple[T | Union[*Ts], ...]in this case.) I do not think it ever makes sense to upcast the base type itself (e.g. fromtupletoobject.)Footnotes
https://github.com/python/cpython/blob/08d7687094c6acb8c2ea1925a292a94ce1246c82/Objects/abstract.c#L919-L980 ↩
https://github.com/python/cpython/blob/ff4959b6b0cf6da5d2f59260a9103b31c812b0ba/Objects/typeobject.c#L2695-L2724 ↩
Of course only in the context of checking whether to apply the reverse op. ↩
sterliakov commentedon May 6, 2025
Ough, sorry, I was missing the third footnote in your previous comment - I thought you're trying to change
is_subtypebehaviour for all purposes.Yes, at runtime we have essentially an
issubclasscheck, and it really doesn't play well with static generics. Perhaps we should replaceis_subtypewithcovers_at_runtimeto determine whether__rOP__should be called first?randolf-scholz commentedon May 6, 2025
@sterliakov I created a draft PR, but there are still some things unclear #19046
Patch `check_op_reversible` to support `tuple` subclasses. (python#19046