Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling with unpacked args f(*args) selects overload for f() #3685

Open
0xjc opened this issue Jul 11, 2022 · 5 comments
Open

Calling with unpacked args f(*args) selects overload for f() #3685

0xjc opened this issue Jul 11, 2022 · 5 comments
Labels
addressed in next version Issue is fixed and will appear in next published version bug Something isn't working spec compliance

Comments

@0xjc
Copy link

0xjc commented Jul 11, 2022

We are seeing new Pyright failures related to the following:

import asyncio
from typing import Awaitable, List


async def f(a: List[Awaitable[int]]):
    b = await asyncio.gather(*a)
    reveal_type(b)  # Type of "b" is "tuple[()]"

The type checking now incorrectly assumes that b is empty, leading to potential errors down the line.

To check that this is not an issue with the asyncio type stubs I tried the following:

from typing import Any, List, Tuple, overload


@overload
def f() -> Tuple[()]:
    ...


@overload
def f(__arg1: Any) -> Tuple[bool]:
    ...


def f(*args: Any) -> Tuple[bool, ...]:
    return tuple(True for _ in args)


def g(a: List[int]):
    b = f(*a)
    reveal_type(b)  # Type of "b" is "Tuple[()]"

Pyright version: 1.1.258
Python version: 3.8.10

@erictraut
Copy link
Collaborator

erictraut commented Jul 11, 2022

This is apparently due to a recent change in the typeshed stub tasks.pyi. Specifically, someone recently added a new overload for asyncio.gather and placed it at the beginning of the overload list.

    @overload
    def gather(*, return_exceptions: bool = ...) -> Future[tuple[()]]: ...

I think pyright is doing the right thing here from a type checking perspective. This overload should probably be moved down to the bottom of the overload list. Feel free to file a bug in the typeshed issue tracker.

@erictraut erictraut added the as designed Not a bug, working as intended label Jul 11, 2022
@0xjc
Copy link
Author

0xjc commented Jul 11, 2022

@erictraut This behavior still seems unexpected to me. I fixed my example to provide an overload for the variadic case:

from typing import Any, List, Tuple, overload


@overload
def f() -> Tuple[()]:
    ...


@overload
def f(*args: Any) -> Tuple[bool, ...]:
    ...


def f(*args: Any) -> Tuple[bool, ...]:
    return tuple(True for _ in args)


def g(a: List[int]):
    reveal_type(f())
    reveal_type(f(1))
    reveal_type(f(*a))
    reveal_type(f(1, *a))

pyright output:

  /tmp/testing.py:19:17 - information: Type of "f()" is "Tuple[()]"
  /tmp/testing.py:20:17 - information: Type of "f(1)" is "Tuple[bool, ...]"
  /tmp/testing.py:21:17 - information: Type of "f(*a)" is "Tuple[()]"
  /tmp/testing.py:22:17 - information: Type of "f(1, *a)" is "Tuple[bool, ...]"

mypy output:

testing.py:19: note: Revealed type is "Tuple[]"
testing.py:20: note: Revealed type is "builtins.tuple[builtins.bool, ...]"
testing.py:21: note: Revealed type is "builtins.tuple[builtins.bool, ...]"
testing.py:22: note: Revealed type is "builtins.tuple[builtins.bool, ...]"

The mypy output is what I would expect - could you explain why in pyright, f(*a) selects the f() overload?

@erictraut
Copy link
Collaborator

Pyright picks the first overload signature that is compatible with the supplied arguments.

The unpack operator expands *args into zero or more arguments, so it is compatible with signatures that assume that it will expand to zero.

Both mypy and pyright accept this code.

def func1() -> None:
    pass

def func2(*args: int) -> None:
    func1(*args)

x: Callable[[], None] = func2

So clearly mypy treats *args as compatible with zero-length expansion. But it appears to be inconsistent with this when it selects overloads.

In any case, pyright is working as designed here, so I don't consider this a bug.

@0xjc
Copy link
Author

0xjc commented Jul 11, 2022

Okay, thanks for the explanation. I'll file my issue in the typeshed repo.

@erictraut
Copy link
Collaborator

I recently submitted a draft update to the Python typing spec that details how type checkers should evaluate calls to overloaded functions. The proposed spec covers this case, which pyright and mypy previously handled in an inconsistent manner. Assuming the draft is accepted (which I think it will be), type checker behavior will be consistent for this case. And in particular, pyright will now evaluate the code above in the same way as mypy.

@erictraut erictraut added bug Something isn't working addressed in next version Issue is fixed and will appear in next published version spec compliance and removed as designed Not a bug, working as intended labels Jan 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addressed in next version Issue is fixed and will appear in next published version bug Something isn't working spec compliance
Projects
None yet
Development

No branches or pull requests

2 participants