python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.44k stars 2.82k forks source link

Spurious error when adding two list #15087

Open mishamsk opened 1 year ago

mishamsk commented 1 year ago

Bug Report

Mypy reports Unsupported operand types for + when concatenating two lists, when the second operand is a subtype of the first.

This was previously mentioned in this comment in #5492

Below is a simple concise example

To Reproduce

class A:
    pass

class B(A):
    pass

def foo(a: list[A]) -> None:
    pass

l1: list[A] = [A(), B()]
l2: list[B] = [B()]
l3 = l1 + l2
foo(l2 + l1)  # OK, no error
foo(l3)  # also OK
foo(
    l1 + l2
)  # ERROR: Unsupported operand types for + ("List[A]" and "List[B]")  [operator]mypy(error)

gist link

Expected Behavior No error on the last line

Actual Behavior Reports spurious error

Your Environment

plugins = ["pydantic.mypy"]


- Python version used: 3.10

<!-- You can freely edit this text, please remove all the lines you believe are unnecessary. -->
ikonst commented 1 year ago

Smaller equivalent:

class A:
    pass

class B(A):
    pass

l1: list[A]
l2: list[B]
x: list[A] = l1 + l2

IIUC, the culprit is the two overloads:

class list(Generic[T], ...):
    @overload
    def __add__(self, __value: list[_T]) -> list[_T]: ...
    @overload
    def __add__(self, __value: list[_S]) -> list[_S | _T]: ...

The expression's expected type list[A] helps select the 1st overload and disqualify the 2nd overload. Maybe if mypy reduced A | B to A then it wouldn't disqualify. (Is doing that sound?)

mishamsk commented 1 year ago

@ikonst Thanks for shorter sample. I'd guess that reduction is not generally possible since list is invariant, and the sum of the operation should be treated as a union. Which btw reminds that in my example passing l3 to foo should have produced an error but somehow it didn't...

Surprised to learn that mypy uses the target var type to select appropriate overload. Didn't know that it works this way. Why so?

ikonst commented 1 year ago

I'd guess that reduction is not generally possible since list is invariant

It seems that if you type-annotate, then mypy would agree with you:

l1: list[A]
l2: list[B]
l3 = l1 + l2
reveal_type(l3)  # builtins.list[Union[__main__.B, __main__.A]]
x: list[A] = l3

However, this won't work:

x: list[B] = l3

nor this:

x: list[A] = l2

so invariance is maintained, but list[A | B] to list[A] is apparently safe.