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

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

Closed
erictraut opened this issue Jul 24, 2024 · 4 comments · Fixed by #17678
Assignees
Labels
bug mypy got something wrong topic-type-narrowing Conditional type narrowing / binder topic-typeguard TypeGuard / PEP 647

Comments

@erictraut
Copy link

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.

@erictraut erictraut added the bug mypy got something wrong label Jul 24, 2024
@sobolevn
Copy link
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`

@sobolevn sobolevn 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 Jul 24, 2024
@sobolevn sobolevn added topic-type-narrowing Conditional type narrowing / binder topic-typeguard TypeGuard / PEP 647 labels Jul 24, 2024
@sobolevn
Copy link
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
Copy link
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
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-type-narrowing Conditional type narrowing / binder topic-typeguard TypeGuard / PEP 647
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants