Skip to content

Generic Callable return type inferred as first overload from overloaded function #21560

@rtandy

Description

@rtandy

Apologies in advance if this duplicates an existing issue. I reviewed a bunch, but none seemed like an exact match. #15015 and #15737 were the closest.

https://mypy-play.net/?mypy=latest&python=3.13&gist=24cbdbfe356ffc9d12b75d5960dc2c4c

from collections.abc import Callable
from typing import Any, TypeVar, overload

T = TypeVar('T')

def call(func: Callable[..., T], *args, **kwargs) -> T:
    return func(*args, **kwargs)

@overload
def f(x: int) -> int:
    ...
@overload
def f(x: str) -> str:
    ...

def f(x):
    return x

reveal_type(call(f, 1))
reveal_type(call(f, "a"))

Expected result: I expected that mypy would either:

  • evaluate the overload for each call and reveal int for the first call, str for the second; or
  • raise an error that T can't be resolved.
  • (or maybe reveal int | str? or even Any? would prefer one of the first two though.)

Actual result: mypy silently uses the return type from the first overload, does not raise any error.

main.py:19: note: Revealed type is "int"
main.py:20: note: Revealed type is "int"
Success: no issues found in 1 source file

My actual use case is that I wanted to write:

# requires: boto3, boto3-stubs[logs]
import functools
import boto3
get_client = functools.cache(boto3.Session().client)
reveal_type(get_client('logs'))
# expected: Revealed type is "mypy_boto3_logs.client.CloudWatchLogsClient"
# actual: Revealed type is "mypy_boto3_accessanalyzer.client.AccessAnalyzerClient" if mypy-boto3-accessanalyzer is installed, "Any" otherwise

boto3.Session.client is an overloaded method. As above, mypy didn't raise any error, but just took the first overload.

The relevant declaration of functools.cache: https://github.com/python/typeshed/blob/1d3abc42077c2ea47985363a4fab387357f3aa77/stdlib/functools.pyi#L263

def cache(user_function: Callable[..., _T], /) -> _lru_cache_wrapper[_T]: ...

If the entire Callable is used (with or without ParamSpec), mypy is able to see the overloads. I don't need the other methods of _lru_cache_wrapper, so this workaround works for me:

from collections.abc import Callable
import functools
from typing import Any, TypeVar, cast

import boto3

T = TypeVar('T', bound=Callable[..., Any])

def cache(func: T) -> T:
    return cast(T, functools.cache(func))

get_client = cache(boto3.Session().client)
reveal_type(get_client('logs'))
# Revealed type is "mypy_boto3_logs.client.CloudWatchLogsClient"

Thank you for maintaining mypy!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions