Skip to content

Incorrect type inference with __radd__ of tuple subtype against literal tuples. #19006

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

Open
randolf-scholz opened this issue Apr 30, 2025 · 1 comment
Labels
bug mypy got something wrong

Comments

@randolf-scholz
Copy link
Contributor

Bug Report

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 testing against tuple literals.

To Reproduce

https://mypy-play.net/?mypy=latest&python=3.12&gist=2a3598efce6ff75cad67822cb5436b53

from typing import reveal_type, assert_type

class Size(tuple[int, ...]):
    def __add__(self, other: tuple[int, ...], /) -> "Size": return Size()  # type: ignore[override]
    def __radd__(self, other: tuple[int, ...], /) -> "Size": return Size()


tup: tuple[int, ...] = (1, 2)
size: Size = Size([3, 4])

# __add__
assert_type(size + tup, Size)  # ✅
assert_type(size + (), Size)  # ✅
assert_type(size + (1, 2), Size)  # ✅
# __radd__
assert_type(tup + size, Size)  # ✅
assert_type(() + size, Size)  # ❌
assert_type((1, 2) + size, Size)  # ❌

# Runtime Checks pass
# __add__
assert type(size + tup) is Size  # ✅
assert type(size + ()) is Size  # ✅
assert type(size + (1, 2)) is Size  # ✅
# __radd__
assert type(tup + size) is Size  # ✅
assert type(() + size) is Size  # ✅
assert type((1, 2) + size) is Size  # ✅
assert type((1,2) + size) is Size  # ✅

The bug does seem to be tuple-specific, for instance it does not appear with integer literals: https://mypy-play.net/?mypy=latest&python=3.12&gist=da0763e25cd0654d1a8b8b0b67291bc5

Expected Behavior

All assert_type in the example above should succeed.

@sterliakov
Copy link
Collaborator

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

if isinstance(proper_left_type, TupleType) and e.op == "+":
left_add_method = proper_left_type.partial_fallback.type.get("__add__")
if left_add_method and left_add_method.fullname == "builtins.tuple.__add__":
proper_right_type = get_proper_type(self.accept(e.right))
if isinstance(proper_right_type, TupleType):
right_radd_method = proper_right_type.partial_fallback.type.get("__radd__")
if right_radd_method is None:
# One cannot have two variadic items in the same tuple.
if (
find_unpack_in_list(proper_left_type.items) is None
or find_unpack_in_list(proper_right_type.items) is None
):
return self.concat_tuples(proper_left_type, proper_right_type)
elif (
PRECISE_TUPLE_TYPES in self.chk.options.enable_incomplete_feature
and isinstance(proper_right_type, Instance)
and self.chk.type_is_iterable(proper_right_type)
):
# Handle tuple[X, Y] + tuple[Z, ...] = tuple[X, Y, *tuple[Z, ...]].
right_radd_method = proper_right_type.type.get("__radd__")
if (
right_radd_method is None
and proper_left_type.partial_fallback.type.fullname == "builtins.tuple"
and find_unpack_in_list(proper_left_type.items) is None
):
item_type = self.chk.iterable_item_type(proper_right_type, e)
mapped = self.chk.named_generic_type("builtins.tuple", [item_type])
return proper_left_type.copy_modified(
items=proper_left_type.items + [UnpackType(mapped)]
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

2 participants