ty icon indicating copy to clipboard operation
ty copied to clipboard

Emit error when unpacking a not-iterable argument in a call

Open phistep opened this issue 4 months ago • 3 comments

Summary

from typing import Generic, TypeAlias, TypeVar


def foo(a: str, b: int, c: int, d: str): ...


VType: TypeAlias = int | tuple[int, int]
V = TypeVar("V", bound=VType)


class Foo(Generic[V]):
    v: V

    def __init__(self, v: V):
        self.v = v


D = dict(
    a=Foo(v=1),
    b=Foo(v=(2, 2)),
)
foo("foo", *D["b"], d="bar")

reports

error[parameter-already-assigned]: Multiple values provided for parameter `d` of function `foo`
  --> src/vhsh/test.py:22:21
   |
20 |     b=Foo(v=(2, 2)),
21 | )
22 | foo("foo", *D["b"], d="bar")
   |                     ^^^^^^^
   |
info: rule `parameter-already-assigned` is enabled by default

Found 1 diagnostic

when actually d is only passed once and D["b"] is unpacked into b and c.

This is similar to #2250 and #1985 but produces an error message and not only misleading inlays.

Image
foo("foo", D["b"][0], D["b"][1], d="bar")

gives the expected[^1] errors

error[non-subscriptable]: Cannot subscript object of type `Foo[int]` with no `__getitem__` method
  --> src/vhsh/test.py:22:12
   |
20 |     b=Foo(v=(2, 2)),
21 | )
22 | foo("foo", D["b"][0], D["b"][1], d="bar")
   |            ^^^^^^^^^
   |
info: rule `non-subscriptable` is enabled by default

error[non-subscriptable]: Cannot subscript object of type `Foo[tuple[int, int]]` with no `__getitem__` method
  --> src/vhsh/test.py:22:12
   |
20 |     b=Foo(v=(2, 2)),
21 | )
22 | foo("foo", D["b"][0], D["b"][1], d="bar")
   |            ^^^^^^^^^
   |
info: rule `non-subscriptable` is enabled by default

error[non-subscriptable]: Cannot subscript object of type `Foo[int]` with no `__getitem__` method
  --> src/vhsh/test.py:22:23
   |
20 |     b=Foo(v=(2, 2)),
21 | )
22 | foo("foo", D["b"][0], D["b"][1], d="bar")
   |                       ^^^^^^^^^
   |
info: rule `non-subscriptable` is enabled by default

error[non-subscriptable]: Cannot subscript object of type `Foo[tuple[int, int]]` with no `__getitem__` method
  --> src/vhsh/test.py:22:23
   |
20 |     b=Foo(v=(2, 2)),
21 | )
22 | foo("foo", D["b"][0], D["b"][1], d="bar")
   |                       ^^^^^^^^^
   |
info: rule `non-subscriptable` is enabled by default

Found 4 diagnostics
Image

[^1]: sadly unsatisfactory due to impossible type inference w/o TypedDict.

Version

ty 0.0.7 (cf82a04b5 2025-12-24)

phistep avatar Dec 28 '25 23:12 phistep

@dhruvmanila do you know what's happening here?

MichaReiser avatar Dec 29 '25 11:12 MichaReiser

I think this might be related to https://github.com/astral-sh/ty/issues/1584, what's happening is that D["b"] cannot be unpacked because it's not an iterable (the type is Foo[int] | Foo[tuple[int, int]] as seen in the inlay hint of D), so it must be unpacking into Unknowns, but we don't know how many elements are there in the unpacking so it has matched all of the remaining arguments by the time we reach to d="bar" which is why you're seeing parameter-already-assigned error.

D["b"] is unpacked into b and c.

I don't think this is correct, refer to my previous paragraph. For a literal dictionary creation, ty will infer the general type of dict[str, <union of value types>].

dhruvmanila avatar Dec 29 '25 12:12 dhruvmanila

Yeah, the implied request here is to do some kind of implicit TypedDict inference of dict literals -- which we could do, but it's quite tricky to handle subsequent mutation without either introducing false positives or false negatives. This is tracked in https://github.com/astral-sh/ty/issues/1248

Barring that, it does seem like there are two bugs in ty here. One is discussed above (and tracked in #1584): given we don't know which arguments D["b"] will unpack to, we should prefer to assume a valid call, not assume it eats all arguments, including those otherwise explicitly provided.

The second is, we should emit an error on the attempt to unpack D["b"] (as pyright does) -- that would help clarify what's going on. We can use this issue to track that.

carljm avatar Dec 29 '25 16:12 carljm

Thank you for replying so quicky!

@dhruvmanila

D["b"] is unpacked into b and c.

I don't think this is correct, refer to my previous paragraph. For a literal dictionary creation, ty will infer the general type of dict[str, <union of value types>].

I'm sorry, I made a typo when boiling down my code to the MWE. I meant to pass .v acutally, so that the unpacking would make sense

D["b"].v (sic!) is unpacked into b and c.

foo("foo", *D["b"].v, d="bar")
Image

Then the report is

error[invalid-argument-type]: Argument to function `foo` is incorrect
  --> test.py:22:12
   |
20 |     b=Foo(v=(2, 2)),
21 | )
22 | foo("foo", *D["b"].v, d="bar")
   |            ^^^^^^^^^ Expected `str`, found `int`
   |
info: Function defined here
 --> test.py:4:5
  |
4 | def foo(a: str, b: int, c: int, d: str): ...
  |     ^^^                         ------ Parameter declared here
  |
info: rule `invalid-argument-type` is enabled by default

error[parameter-already-assigned]: Multiple values provided for parameter `d` of function `foo`
  --> test.py:22:23
   |
20 |     b=Foo(v=(2, 2)),
21 | )
22 | foo("foo", *D["b"].v, d="bar")
   |                       ^^^^^^^
   |
info: rule `parameter-already-assigned` is enabled by default

Found 2 diagnostics

Which is wrong and misleading in a different way...


Whereas this givers the correct error message (where it is sad but understanable that no implicit TypedDict style inference can take place)

foo("foo", D["b"].v[0], D["b"].v[1], d="bar")
error[not-subscriptable]: Cannot subscript object of type `int` with no `__getitem__` method
  --> test.py:24:12
   |
22 | v = D["b"].v
23 | # foo("foo", *D["b"].v, d="bar")
24 | foo("foo", D["b"].v[0], D["b"].v[1], d="bar")
   |            ^^^^^^^^^^^
   |
info: rule `not-subscriptable` is enabled by default

error[not-subscriptable]: Cannot subscript object of type `int` with no `__getitem__` method
  --> test.py:24:25
   |
22 | v = D["b"].v
23 | # foo("foo", *D["b"].v, d="bar")
24 | foo("foo", D["b"].v[0], D["b"].v[1], d="bar")
   |                         ^^^^^^^^^^^
   |
info: rule `not-subscriptable` is enabled by default

phistep avatar Dec 30 '25 23:12 phistep

@phistep I think that your "wrong and misleading in a different way" example would still be covered by #1584 -- we shouldn't map the unpacking to argument d when it is provided explicitly.

carljm avatar Jan 06 '26 02:01 carljm