Skip to content

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

Open
@erictraut

Description

@erictraut

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.

Activity

sobolevn

sobolevn commented on Jul 24, 2024

@sobolevn
Member

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`
changed the title [-]Overloads not resolved correctly when argument is `Any` and return types use `TypeIs`[/-] [+]Overloads not resolved correctly when argument is `Any` and return types use `TypeGuard` or `TypeIs`[/+] on Jul 24, 2024
sobolevn

sobolevn commented on Jul 24, 2024

@sobolevn
Member

The problem is that we don't check for overloads here:

mypy/mypy/checker.py

Lines 5873 to 5890 in db9837f

if literal(expr) == LITERAL_TYPE:
# Note: we wrap the target type, so that we can special case later.
# Namely, for isinstance() we use a normal meet, while TypeGuard is
# considered "always right" (i.e. even if the types are not overlapping).
# Also note that a care must be taken to unwrap this back at read places
# where we use this to narrow down declared type.
if node.callee.type_guard is not None:
return {expr: TypeGuardedType(node.callee.type_guard)}, {}
else:
assert node.callee.type_is is not None
return conditional_types_to_typemaps(
expr,
*self.conditional_types_with_intersection(
self.lookup_type(expr),
[TypeRange(node.callee.type_is, is_upper_bound=False)],
expr,
),
)

erictraut

erictraut commented on Jul 24, 2024

@erictraut
Author

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

sobolevn commented on Aug 13, 2024

@sobolevn
Member

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

self-assigned this
on Aug 13, 2024
davidhalter

davidhalter commented on Feb 11, 2025

@davidhalter

There is an inconsistency left where currently TypeIs/TypeGuard narrows in case of multi-Any matches, while this is not done in normal overloads. I find this strange and would prefer that it is consistent.

@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)  # Currently int | str


@overload
def func3(x: str) -> str:
    ...

@overload
def func3(x: int) -> int:
    ...

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

def func4(val: Any):
    reveal_type(func3(val))  # Currently Any

Playground (used Mypy 1.15)

(Edited, because I messed up)

added a commit that references this issue on Feb 11, 2025
sobolevn

sobolevn commented on Feb 11, 2025

@sobolevn
Member

@davidhalter this is unrelated, please create a new issue (if there no existing one).

davidhalter

davidhalter commented on Feb 11, 2025

@davidhalter

@sobolevn I created #18659

Why is this unrelated? Before this change it would narrow to the first overload. Now it narrows to a union. The consistent way would have been not narrowing at all.

sterliakov

sterliakov commented on May 29, 2025

@sterliakov
Collaborator

The first attempt was reverted due to an unexpected regression in #19161, reopening.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugmypy got something wrongtopic-type-narrowingConditional type narrowing / bindertopic-typeguard-typeisTypeGuard / TypeIs / PEP 647 / PEP 742

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    Participants

    @davidhalter@sobolevn@erictraut@sterliakov

    Issue actions

      Overloads not resolved correctly when argument is `Any` and return types use `TypeGuard` or `TypeIs` · Issue #17579 · python/mypy