I looked a bit into this and managed to create an example that's isolated from the max type stub. The type stub is correct!
fromtypingimportList, Union, Callable, TypeVar, reveal_type_T1=TypeVar("_T1")
_T2=TypeVar("_T2")
defbar(a: _T1, b: _T2, function: Callable[[_T2], None]) ->Union[_T1, _T2]:
raiseNotImplementedError()
# this does not cause an issue,result_1=bar(42, [42], lambdaa_list: a_list.append(42))
reveal_type(result_1) # note: Revealed type is "Union[builtins.int, builtins.list[builtins.int]]"# this is the same call, we set the same type, but it causes an issue with the lambda bodyresult_2: Union[int, List[int]] =bar(
42,
[42],
lambdaa_list: a_list.append(42) # error: Item "int" of "int | list[int]" has no attribute "append" [union-attr]
)
Looking at the code, this seems to be because ExpressionChecker.infer_function_type_arguments_using_context() somehow infers the type variables from the return type, not from known passed argument values. I'll see if I can put up an MR for that.
Thanks for taking a deeper look into this @pfaion! Fair warning, this may be a tricky issue.
This has to do with the way mypy infers the types of generic callables, and in particular the relative "trust" given to the type context vs the argument types.
Currently, mypy infers the type of generic callables in two steps: first using the type context to infer against type variables in the return type, and second using the argument types to infer any remaining type variables.
The issue in this case is that, during the first step, mypy can use the type context to fully resolve the type variables into a broad supertype, which then gets propagated down into the argument checks, which then fail due to the inferred type being too broad.
The tricky bit is that, on its own, this "trusting the type context" behavior is a intended feature. It's what lets mypy use return types and explicit type annotations (which are generally very trustworthy) to type check inner expressions (which is often where the actual problems are). For example, it's what makes this possible:
deffoo(x: list[int]) ->None: ...
foo([1, 2, 3, "4", 5]) # List item 3 has incompatible type "str"; expected "int"
Details
Here, mypy needs to infer the type of the list being passed to foo. From the type context, it can see that the ultimate expected type is list[int]. This lets mypy resolve the type of the list constructor from def [T] (*T) -> list[T] to def (*int) -> list[int] (via inferring the against the return type using the type context). It can then check the individual arguments against the expected type int, allowing it to catch the unexpected str argument. In the alternative case, mypy could infer the type of the list just from the argument types. This would result in it deducing the type list[object], leading to the more opaque error Argument 1 to "foo" has incompatible type "list[object]"; expected "list[int]".
The fix to this issue will probably involve reworking the way that mypy uses the type context during type inference. I'm guessing this will invovle some major code changes, and probably has a high risk of breaking existing behavior. Good luck to whoever attempts it! :-)
I think if you'd infer the type from the arguments, you might be able to provide something like: Argument 1 to "foo" has incompatible type "list[int | str]"; expected "list[int]". Wouldn't that be somewhat useful?
Or maybe an exception needs to be made for "constructors" in this context? The example you gave sounded like you're interested in errors coming from inner expressions before errors coming from outer expressions. I'm wondering if that really is generally the case, or if that's an exception for when the inner expression is about constructing a type, where that would be more helpful?
I might try understanding the surrounding code a bit more. But thanks for the heads-up, I understand now how tricky this is. I was a bit confused why the type inference was started from the return type, but I thought there must be some explanation behind that!
Activity
pfaion commented on Sep 29, 2024
I looked a bit into this and managed to create an example that's isolated from the max type stub. The type stub is correct!
Playground link
Looking at the code, this seems to be because ExpressionChecker.infer_function_type_arguments_using_context() somehow infers the type variables from the return type, not from known passed argument values. I'll see if I can put up an MR for that.
brianschubert commented on Sep 29, 2024
Looks like a duplicate of #14664
Thanks for taking a deeper look into this @pfaion! Fair warning, this may be a tricky issue.
This has to do with the way mypy infers the types of generic callables, and in particular the relative "trust" given to the type context vs the argument types.
Currently, mypy infers the type of generic callables in two steps: first using the type context to infer against type variables in the return type, and second using the argument types to infer any remaining type variables.
The issue in this case is that, during the first step, mypy can use the type context to fully resolve the type variables into a broad supertype, which then gets propagated down into the argument checks, which then fail due to the inferred type being too broad.
The tricky bit is that, on its own, this "trusting the type context" behavior is a intended feature. It's what lets mypy use return types and explicit type annotations (which are generally very trustworthy) to type check inner expressions (which is often where the actual problems are). For example, it's what makes this possible:
Here, mypy needs to infer the type of the list being passed to foo. From the type context, it can see that the ultimate expected type is list[int]. This lets mypy resolve the type of the list constructor from def [T] (*T) -> list[T] to def (*int) -> list[int] (via inferring the against the return type using the type context). It can then check the individual arguments against the expected type int, allowing it to catch the unexpected str argument. In the alternative case, mypy could infer the type of the list just from the argument types. This would result in it deducing the type list[object], leading to the more opaque error Argument 1 to "foo" has incompatible type "list[object]"; expected "list[int]".
The fix to this issue will probably involve reworking the way that mypy uses the type context during type inference. I'm guessing this will invovle some major code changes, and probably has a high risk of breaking existing behavior. Good luck to whoever attempts it! :-)
pfaion commented on Sep 29, 2024
Hmmm... ok, that is unfortunate.
I think if you'd infer the type from the arguments, you might be able to provide something like: Argument 1 to "foo" has incompatible type "list[int | str]"; expected "list[int]". Wouldn't that be somewhat useful?
Or maybe an exception needs to be made for "constructors" in this context? The example you gave sounded like you're interested in errors coming from inner expressions before errors coming from outer expressions. I'm wondering if that really is generally the case, or if that's an exception for when the inner expression is about constructing a type, where that would be more helpful?
I might try understanding the surrounding code a bit more. But thanks for the heads-up, I understand now how tricky this is. I was a bit confused why the type inference was started from the return type, but I thought there must be some explanation behind that!
Extend special case for context-based typevar inference to typevar un…