basedpyright icon indicating copy to clipboard operation
basedpyright copied to clipboard

False positive `reportAssignmentType` with `list.__add__` type widening from a newly-created object

Open Phrogz opened this issue 5 months ago • 13 comments

Description

foo: list[str] = ["a", "b"]
bar: list[str | float] = foo + ["c"]     # incorrectly shows a type-widening error here
baz: list[str | float] = ["a", "b", "c"] # type-widening is correctly accepted here
bat: list[str | float] = foo             # correctly shows a type-widening error here

The bar line should be treated as the baz, as list + list creates a new list with no other references to it; mutating bar does not/cannot affect foo.

Link to BasedPyright Playground Example

Occurs in Pyright as well; filed upstream as Pyright bug #10943.

Phrogz avatar Sep 18 '25 17:09 Phrogz

hmm, is this a typeshed issue?

KotlinIsland avatar Sep 19 '25 05:09 KotlinIsland

I think it would require extra steps of inference (likely a performance limitation) or some kind of ownership model in the type system to know that there are no other references to the value returned by list.__add__

beauxq avatar Sep 19 '25 15:09 beauxq

can you provide an example of this being unsound? as far as I can think:

class list[T]:
    def __add__[O](self, other: list[O]) -> list[T | O]:
        result = []
        result.extend(self)
        result.extend(other)
        return result

this seems valid to me

KotlinIsland avatar Sep 19 '25 17:09 KotlinIsland

I don't think that's unsound. But how are we supposed to get the inference of the type variables that we need from the code in the OP?

beauxq avatar Sep 19 '25 20:09 beauxq

I haven't investigated the signature yet, but there is no issue inferring other as list[float]

Code sample in basedpyright playground

KotlinIsland avatar Sep 20 '25 01:09 KotlinIsland

I haven't investigated the signature yet, but there is no issue inferring other as list[float]

The OP has ["c"] in the position that would need to be list[float]

beauxq avatar Sep 20 '25 02:09 beauxq

ah, my mistake, it's still normal bidirectional type inference, should be fine

Code sample in basedpyright playground

KotlinIsland avatar Sep 20 '25 04:09 KotlinIsland

    # Overloading looks unnecessary, but is needed to work around complex mypy problems
    @overload
    def __add__(self, value: list[_T], /) -> list[_T]: ...
    @overload
    def __add__(self, value: list[_S], /) -> list[_S | _T]: ...

so this is a nonsense mypy issue, i think we update the vendored typeshed and see what comes out of the primer

KotlinIsland avatar Sep 20 '25 05:09 KotlinIsland

ah, my mistake, it's still normal bidirectional type inference, should be fine

Code sample in basedpyright playground

That sample is covariant.

This one is invariant: basedpyright playground

beauxq avatar Sep 20 '25 19:09 beauxq

i think this needs more than just a typeshed update. there seems to be some special casing when using the + operator that results in different behavior to when the dunder is called like a regular method:

-     # Overloading looks unnecessary, but is needed to work around complex mypy problems
-     @overload
-     def __add__(self, value: list[_T], /) -> list[_T]: ...
-     @overload
-     def __add__(self, value: list[_S], /) -> list[_S | _T]: ...
+     def __add__(self, value: list[_S | _T], /) -> list[_S | _T]:
        """Return self+value."""
        ...
foo: list[str] = ["a", "b"]
bar: list[str | float] = foo.__add__(["c"])  # now works
baz: list[str | float] = foo + ["c"] # still errors

DetachHead avatar Sep 21 '25 01:09 DetachHead

i fixed it: Code sample in basedpyright playground

we need to capture the expected type as well

KotlinIsland avatar Sep 21 '25 03:09 KotlinIsland

extreme sus detected: Code sample in basedpyright playground

looks like the order of the union is affecting the behaviour

  • #1492

KotlinIsland avatar Sep 21 '25 04:09 KotlinIsland

i think this needs more than just a typeshed update. there seems to be some special casing when using the + operator that results in different behavior to when the dunder is called like a regular method:

-     # Overloading looks unnecessary, but is needed to work around complex mypy problems
-     @overload
-     def __add__(self, value: list[_T], /) -> list[_T]: ...
-     @overload
-     def __add__(self, value: list[_S], /) -> list[_S | _T]: ...
+     def __add__(self, value: list[_S | _T], /) -> list[_S | _T]:
        """Return self+value."""
        ...
foo: list[str] = ["a", "b"]
bar: list[str | float] = foo.__add__(["c"])  # now works
baz: list[str | float] = foo + ["c"] # still errors

#1493

DetachHead avatar Sep 21 '25 06:09 DetachHead