python / mypy

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

Overloads not resolved correctly when argument is `Any` and return types use `TypeGuard` or `TypeIs` #17579

Open erictraut opened 1 month ago

erictraut commented 1 month ago

When overload matching is ambiguous due to an Any argument, mypy typically looks at the return types of the potential matches. If the return types differ, it evaluates the return type as Any to preserve the "gradual type guarantee". It apparently doesn't do this when the return types of the potential overload matches use TypeIs. See the example below, where mypy matches the first overload rather than detecting the ambiguity and evaluating Any.

from typing import Any, overload
from typing_extensions import TypeIs

@overload
def func1(x: str) -> TypeIs[str]:
    ...

@overload
def func1(x: int) -> TypeIs[int]:
    ...

def func1(x: Any) -> Any:
    return True

def func2(val: Any):
    if func1(val):
        reveal_type(val) # Should be Any

I discovered this problem because pyright has the same issue. I'm guessing that the underlying cause of the bug in mypy is similar. Pyright's logic was treating TypeIs[T] as equivalent to bool in this case, but TypeIs[T] should be treated as a subtype of bool. Also, TypeIs[X] is not equivalent to TypeIs[Y] unless X is equivalent to Y.

This bug affects a recent change in typeshed to the dataclasses.isdataclass function, as discussed here.

sobolevn commented 1 month ago

The same happens with TypeGuard as well:

from typing import Any, overload
from typing_extensions import TypeGuard

@overload
def func1(x: str) -> TypeGuard[str]:
    ...

@overload
def func1(x: int) -> TypeGuard[int]:
    ...

def func1(x: Any) -> Any:
    return True

def func2(val: Any):
    if func1(val):
        reveal_type(val) # Should be `Any`, but is `str`
sobolevn commented 1 month ago

The problem is that we don't check for overloads here: https://github.com/python/mypy/blob/db9837fa240918fa90f2d9a3952314bd3207ed20/mypy/checker.py#L5873-L5890

erictraut commented 1 month ago

The same happens if TypeGuard and TypeIs are both used in an overload. This is an edge case that's unlikely to ever be used in real-world code, but it might as well be fixed along with the other cases.

from typing import Any, overload
from typing_extensions import TypeGuard, TypeIs

@overload
def func1(x: int | str) -> TypeIs[int]:
    ...

@overload
def func1(x: list) -> TypeGuard[int]:
    ...

def func1(x: Any) -> Any:
    return True

def func2(val: Any):
    if func1(val):
        reveal_type(val) # Should be `Any`, but is `int`
sobolevn commented 3 weeks ago

I have a prototype implementation, will create a PR tomorrow 🎉