Overload not resolved when the function is passed through a `ParamSpec` generic
Summary
When an overloaded function is passed to a generic function that uses ParamSpec, ty does not resolve the overload against the actual arguments. It returns a union of the overload return types instead. mypy and pyright both resolve it correctly.
from typing import Callable, Literal, overload, reveal_type
@overload
def f(*, flag: Literal[False]) -> str: ...
@overload
def f(*, flag: Literal[True] = ...) -> int: ...
def f(*, flag: bool = True) -> int | str:
return 1 if flag else 'a'
def call[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
return fn(*args, **kwargs)
reveal_type(f()) # ty: int mypy: int pyright: int
reveal_type(call(f)) # ty: str | int mypy: int pyright: intThe last line should be int. No flag argument is passed, so the second overload applies via its default.
The usual way to hit this is Executor.submit, whose first parameter is Callable[P, T]:
takes_int(executor.submit(f).result())
# error[invalid-argument-type]: Expected `int`, found `str | int`With three or more overloads ty sometimes picks the last overload's return type rather than a union, but the effect is the same.
This looks like the other half of #2383. That issue was closed when ParamSpec support landed, and the argument is indeed accepted now — but the return type is still wrong.
Impact: we evaluated ty on a ~1600-file codebase that is clean under mypy strict. This one pattern produces 437 of 689 diagnostics, because our database accessors are overloaded on a Literal[bool] flag that selects the return type, and they are usually called through executor.submit.
Possibly related: #2568, #3415, #2799.
Version
ty 0.0.81
Source: astral-sh/ty