mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Tuple of iterables are not narrowed after converting it to tuple of tuple

Open airallergy opened this issue 1 year ago • 6 comments

Bug Report

If there is a function that takes *args: Iterable[float] as an argument, mypy still thinks it is a tuple of iterates after converting it to tuple of tuple via either map or tuple comprehension.

To Reproduce

https://mypy-play.net/?mypy=latest&python=3.11&gist=f70e818d8b82887c5d7f0b19cf51b504

from typing import Iterable


def foo(*args: Iterable[float]):
    x = tuple(map(tuple, args))
    reveal_type(x)  # Revealed type is "builtins.tuple[builtins.tuple[builtins.float, ...], ...]"

    args = tuple(map(tuple, args))
    reveal_type(args)  # Revealed type is "builtins.tuple[typing.Iterable[builtins.float], ...]"

Expected Behavior

args should have the same type as x.

Actual Behavior

args's type is not narrowed down after conversion.

Your Environment

  • Mypy version used: 1.10
  • Mypy command-line flags: N/A
  • Mypy configuration options from mypy.ini (and other config files): N/A
  • Python version used: 3.11

airallergy avatar Apr 29 '24 23:04 airallergy

Mypy is correct here. The annotation Iterable[float] for *args means that each argument passed to *args must be an Iterable[float]. If your intention is that each argument passed to *args is a float, then you should simply specify *args: float. See this section of the Python typing spec for details.

erictraut avatar Apr 29 '24 23:04 erictraut

No, my intention is args being tuple[Iterable[float], …] when passed in. float is possibly not important here, i.e. can be any type, it is just my actual case.

I need to convert such args to tuple[tuple[float, …], …], which is what I did for both x (assign to a new variable) and args (reassignment) inside foo, I expect they both have the converted/narrowed type, but the reassignment somehow does not work.

Hope it is clearer now.

airallergy avatar Apr 29 '24 23:04 airallergy

Ah, I missed the fact that you were assigning the expression back to args in the second case.

I agree that mypy should be narrowing the type of args on assignment.

Incidentally, the same bug exists in pyright. I've logged a similar bug for pyright.

erictraut avatar Apr 30 '24 01:04 erictraut

@erictraut, thanks for the investigation, I saw your comments in the pyright repo.

I feel that it is still a bug and does not fall into bidirectional type inference. My understanding from the mypy document is that bidirectional inference is involved when assigning a literal value to a previously annotated variable, which is not the case here.

consider another case:

from typing import Iterable


def bar(arg: Iterable[float]):
    arg = tuple(arg)
    reveal_type(arg)

Mypy sees arg converted from Iterable[float] to tuple[float, …] no problem.

airallergy avatar Apr 30 '24 05:04 airallergy

My understanding ... is that bidirectional inference is involved when assigning a literal value to a previously annotated variable.

Bidirectional inference is used whenever the target of an assignment provides additional context about the "expected type". It's not limited to the assignment of literal values.

In any case, I don't consider this a bug, and I've closed the corresponding issue in pyright. I'll leave it to the maintainers of mypy to decide whether they consider this a bug in mypy. In my opinion, it's not.

erictraut avatar Apr 30 '24 05:04 erictraut

Thanks for clarification. In that case, I find this surprising and somewhat against the runtime behaviour.

In my use case, since I explicitly convert the iterable to tuple, I would expect it to be indexable (as it is in runtime), but mypy complains later when I tried to index it. Workarounds like cast seems redundant in such case.

airallergy avatar Apr 30 '24 08:04 airallergy