Description
Bug Report
If I have an @overload
ed 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.
Activity
JelleZijlstra commentedon Dec 21, 2024
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 commentedon Dec 21, 2024
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 commentedon Dec 21, 2024
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?
jackmpcollins commentedon Jan 5, 2025
For the example in the description, mypy (and pyright) correctly infers the types if the parameter type of the second overload is broadened to the union
list[_T] | _T
.I'm not sure why this works. It seems that mypy prefers to match the whole arg type
str | list[str]
to an overload if possible vs matching each part (str
andlist[str]
) to overloads and unioning the results.I think pyright only passes for the example because it ignores the type hint for
arg
fix: revert type annotations on collections.to_set
fix: revert type annotations on collections.to_set
fix: revert type annotations on collections.to_set