python / mypy

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

Variadic tuple unpacking requires a star target -- why? #17282

Open ringohoffman opened 4 months ago

ringohoffman commented 4 months ago

Bug Report

When attempting to unpack a tuple that uses Unpack, if you don't use a star target to capture the variadic tuple as a list, you get the error Variadic tuple unpacking requires a star target.

This is overly strict and prescriptivist and is not even applied consistently between similar type concepts within mypy.

  1. You definitely can do this if the lengths check out.
  2. pyright doesn't raise an error like this. You are simply on your own for length checking the unpacking of indeterminate length tuples, just like you are for list, another indeterminate length unpackable sequence.
  3. In fact, this is also how mypy treats unpacking list. It doesn't raise an error if you don't use a star target.
  4. I have a real use case where I want to do exactly this. I lay out my use case in this bug report to pyright: https://github.com/microsoft/pyright/issues/7987

To Reproduce

from typing import Tuple

from typing_extensions import Unpack

def foo() -> Tuple[int, Unpack[Tuple[str, ...]]]:
    return 1, "bar", "baz"

a, b, c = foo()  # error: Variadic tuple unpacking requires a star target  [misc]

d, e, f = [1, "bar", "baz"]  # OK

Gist URL: https://gist.github.com/mypy-play/4014976ca069e54b5c1fe86a45353f58 Playground URL: https://mypy-play.net/?mypy=master&python=3.12&gist=4014976ca069e54b5c1fe86a45353f58

Expected Behavior

No errors in the above example. a's type should be revealed to be int, and b and c's types should be revealed to be str.

Actual Behavior

An error, and a, b, and c are all revealed to be Any.

TeamSpen210 commented 4 months ago

The error would be correct, since based on the types it could produce an incorrect result. Maybe Mypy should require you to ignore the error, but assume the length is going to match when unpacking the tuple. Alternatively, more verbose code with an assertion works:

tup = foo()
assert len(tup) == 3
a, b, c = tup
sterliakov commented 3 months ago

@TeamSpen210 well, it could, but such treatment should be consistent - either reject all or accept all.

from typing_extensions import Unpack

def foo() -> tuple[int, Unpack[tuple[str, ...]]]:
    return 1, "bar", "baz"

def bar() -> tuple[int|str, ...]:
    return 1, "bar", "baz"

a, b, c = foo()  # E: Variadic tuple unpacking requires a star target  [misc]
p, q, r = bar()

So "unpack tuple of int followed by 0 or more str" is bad, because "0 or more" may be not equal to 2. "Unpack tuple of 0 or more int | str" is considered safe, however. Hmm?

NeilGirdhar commented 2 months ago

I agree that this seems like a bad error. it's pretty common in Python to do things like:

x, = m
x, y = n
# etc.

to both unpack and implicitly assert the length of the list. Could we at least get a separate error code that we can disable?