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

Overload + generic functions + union arguments = inconsistent results (wrong overload is matched?) #18321

Open
sbrudenell opened this issue Dec 21, 2024 · 3 comments
Labels
bug mypy got something wrong topic-overloads topic-type-context Type context / bidirectional inference

Comments

@sbrudenell
Copy link

Bug Report

If I have an @overloaded generic function, and call that function with a variable having union type, I get inconsistent results.

To Reproduce

from typing import TypeVar                                                      
from typing import overload                                                     
from typing_extensions import assert_type                                       
                                                                                
_T = TypeVar("_T")                                                              
                                                                                
@overload                                                                       
def makelist(a: list[_T]) -> list[_T]: ...                                      
@overload                                                                       
def makelist(a: _T) -> list[_T]: ...                                            
def makelist(a: list[_T] | _T) -> list[_T]:                                     
    if isinstance(a, list):                                                     
        return a                                                                
    return [a]                                                                  
                                                                                
# no error                                                                      
assert_type(makelist("a"), list[str])                                           
# no error                                                                      
assert_type(makelist(["a"]), list[str])                                         
                                                                                
arg: str | list[str] = "a"                                                      
# error: Expression is of type "list[str | list[str]]", not "list[str]" [assert-type]
assert_type(makelist(arg), list[str])                                           
                                                                                
def want_list_of_str(a: list[str]) -> None:                                     
    ...                                                                         
                                                                                
# no error, despite error above                                                 
want_list_of_str(makelist(arg))                                                 
list_of_str: list[str] = makelist(arg)       

Expected Behavior

I expect that if the argument has type str | list[str], then makelist(arg) should be of type list[str].

Reason: the first @overload (def makelist(a: list[_T]) -> list[_T]: ...) should match list[str]. The second @overload (def makelist(a: _T) -> list[_T]: ...) should match str. So in all cases of the argument's union type, the return type should be list[str].

I also expect mypy to be consistent about expression types. Since mypy says makelist(str_or_list_of_str) has type list[str | list[str]], then list_of_str: list[str] = makelist(str_or_list_of_str) should raise an error.

Actual Behavior

$ mypy t.py
t.py:23: error: Expression is of type "list[str | list[str]]", not "list[str]"  [assert-type]

Your Environment

  • Mypy version used: mypy 1.14.0 (compiled: yes)
  • Mypy command-line flags: mypy
  • Mypy configuration options from pyproject.toml:
[tool.mypy]
strict = true
  • Python version used: Python 3.10.12

If I had to guess, it seems like mypy is wrongly matching my str | list[str] argument to the broad overload (def makelist(a: _T) -> list[_T]: ...). But I don't understand why it's not consistent.

#17331 also involves overloads and generics. Perhaps it's related.

@sbrudenell sbrudenell added the bug mypy got something wrong label Dec 21, 2024
@JelleZijlstra JelleZijlstra added topic-type-context Type context / bidirectional inference topic-overloads labels Dec 21, 2024
@JelleZijlstra
Copy link
Member

Overload behavior is poorly specified and mypy's behavior is not clearly incorrect here; the second overload accepts any _T, and that's the one it picked. The "inconsistent" results you see are because of mypy's use of type context, a technique that makes it so mypy tries a little harder to match the expected type if it knows what type should be expected.

Possibly something should change here but a change to overload semantics would be tricky. I note that pyright passes all of your tests without errors.

@erictraut
Copy link

As Jelle mentioned, evaluation of calls to overloaded functions is currently underspecified in the Python typing spec, so mypy's behavior here is not clearly correct or incorrect. There is an effort underway to clarify the expected behavior for overloads. See this thread for details. Also, I recently presented the proposed spec in a typing meetup, which can be viewed here. If you have feedback on the proposal, please post to one of those discussion threads. If the latest version of my specification proposal were to be ratified, your code sample above would type check without error by type checkers that comply with the spec.

@sbrudenell
Copy link
Author

Thanks for the discussion links. I'll add my original use case there. It looks like real-world examples are rare.

For the sake of documenting the current state of things: I guess my example is technically incorrect behavior, insofar as it's internally inconsistent. But, the behavior won't change until the new spec is adopted. Is that right?

What I mean is: if I change the overload order in my example, I get error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match]. This implies opinionated, well-defined overload resolution despite PEP-484's ambiguity, inconsistent with my example of matching an arbitrary overload.

But, overload-cannot-match is an accurate check for part of the new spec, and removing it would needlessly admit programs that would become invalid under the new spec.

And the spec is complex, so mypy won't put in effort to implement some opinionated, unofficial-but-probably-correct overload handling until the new spec is agreed upon.

Do I have all that right?

Also, my example of type context inconsistency is very surprising. The two different inferred types don't even overlap. As a user, I can't wrap my head around this, even after trying to read the other topic-type-context issues. If this is really an expected class of error, perhaps it could be documented somewhere?

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-overloads topic-type-context Type context / bidirectional inference
Projects
None yet
Development

No branches or pull requests

3 participants