This seems to work generally, but there are some weird circumstances when it bugs out. It seems most of them appear when a concatenation is given as an argument to another function.
As an example, this is one of the lines that gets flagged:
Due to the callee = self.infer_function_type_arguments_using_context(...), the result of Vec[int] + Vec[int] is inferred as Vec[int | str]1 instead of Vec[int], and then later check_argument_types fails because of that.
If we were to swap and do callee = self.infer_function_type_arguments(...) before callee = self.infer_function_type_arguments_using_context(...) then this bug disappears. However, it would cause lots of other things to break, such as assignments like x: list[object] = [1,2,3].
@JelleZijlstra any pointers on how this could be fixed? It is a rather annoying bug that prevents doing some useful simplifications in typeshed.
It's generally expected that type checking may behave differently when assigning to an intermediate variable (due to type context). It's possible that we can improve the heuristics here, but there's a big risk that we'll make other cases worse. I don't have much knowledge of the tradeoffs made within mypy here but you're welcome to try out some options.
I wouldn't expect things to behave differently depending on whether you use old-style or PEP 695 generics; there might be a bug there.
PEP 695 doesn't make a difference, the difference I saw earlier was because I didn't ensure invariance in T in the example, and it made it covariant with PEP 695
So with this type context stuff as far as I understood it after some debugging is that we want to allow:
x: list[object] = [xforxin [1,2,3]]
Which without context would fail, since list[T] is invariant in T and the RHS would evaluate to list[int].
However, since int is assignable to object, we are allowed to insert an upcast:
Or, equivalently, we could consider putting a lower bound on the type variable in the constructor
# original only has upper bounddeflist_comprehension[T ≤ object](arg: T) ->list[T]: ...
# we can consider the restrictiondeflist_comprehension[object ≤ T ≤ object](arg: T) ->list[T]: ...
Applying to our case
In the OP example, this fails as follows:
The type checker solves Vec[S | int] ≤ Iterable[int | str] and finds S ≤ int | str.
However, Vec[int] is not assignable to Vec[int | str], therefore the upcasting is not valid in this case!
Therefore, to solve the bug, inferring from context is only OK if the original arguments are assignable to the upcasted types.
Shower-thought: I wonder if one could get rid of this context completely by using something similar to interval-arithmetic. Instead of returning a singular type, consider returning a typevar that is constrained to the range of possible values one could obtain by upcasting the arguments. e.g. for [x for x in [1,2,3]] we would get the interval T∈[int, object], and then voila, the result is assignable to list[object].
The bug seems to be that mypy uses the outer return context to specialize a generic callable before it has checked whether the actual arguments still fit that specialized signature.
In OP's Vec example, the outer context pushes the method toward something like:
(Vec[int|str]) ->Vec[int|str]
but the actual argument is still Vec[int]. So that contextual specialization is not a valid call. The same basic issue shows up in the recent min(..., key=...) example from @randolf-scholz : the return context can widen the item type enough that the key function no longer matches.
The PR keeps the existing contextual inference path, but adds a guard: if the context-specialized arguments are not compatible with the actual arguments, mypy can fall back to ordinary argument inference, as long as the ordinary inferred return type still satisfies the original outer context.
I had to keep this fairly narrow. In particular, constructors and special synthetic signatures keep the old behavior. Without that, tests like testAbstractTypeInADict failed, and some constructor/type-alias tests report worse Never-based messages.
Activity
randolf-scholz commented on Jun 26, 2025
EDIT: I was able to reproduce it with PEP 695 syntax and updated the OP accordingly.
randolf-scholz commented on Jun 26, 2025
I believe this is the code responsible for the problem:
mypy/mypy/checkexpr.py
Lines 1772 to 1799 in 5e9d657
Due to the callee = self.infer_function_type_arguments_using_context(...), the result of Vec[int] + Vec[int] is inferred as Vec[int | str]1 instead of Vec[int], and then later check_argument_types fails because of that.
If we were to swap and do callee = self.infer_function_type_arguments(...) before callee = self.infer_function_type_arguments_using_context(...) then this bug disappears. However, it would cause lots of other things to break, such as assignments like x: list[object] = [1,2,3].
@JelleZijlstra any pointers on how this could be fixed? It is a rather annoying bug that prevents doing some useful simplifications in typeshed.
Footnotes
due to the context Iterable[int | str] ↩
randolf-scholz commented on Jun 26, 2025
My gut feeling is that this code should first try checking without context, and only if that fails, consider the context as well.
JelleZijlstra commented on Jun 26, 2025
It's generally expected that type checking may behave differently when assigning to an intermediate variable (due to type context). It's possible that we can improve the heuristics here, but there's a big risk that we'll make other cases worse. I don't have much knowledge of the tradeoffs made within mypy here but you're welcome to try out some options.
I wouldn't expect things to behave differently depending on whether you use old-style or PEP 695 generics; there might be a bug there.
randolf-scholz commented on Jun 26, 2025
PEP 695 doesn't make a difference, the difference I saw earlier was because I didn't ensure invariance in T in the example, and it made it covariant with PEP 695
randolf-scholz commented on Jun 26, 2025
So with this type context stuff as far as I understood it after some debugging is that we want to allow:
Which without context would fail, since list[T] is invariant in T and the RHS would evaluate to list[int].
However, since int is assignable to object, we are allowed to insert an upcast:
Applying to our case
In the OP example, this fails as follows:
Therefore, to solve the bug, inferring from context is only OK if the original arguments are assignable to the upcasted types.
randolf-scholz commented on Jun 26, 2025
Shower-thought: I wonder if one could get rid of this context completely by using something similar to interval-arithmetic. Instead of returning a singular type, consider returning a typevar that is constrained to the range of possible values one could obtain by upcasting the arguments. e.g. for [x for x in [1,2,3]] we would get the interval T∈[int, object], and then voila, the result is assignable to list[object].
randolf-scholz commented on Mar 26, 2026
Another annoying example playground
rheard commented on Aug 2, 2026
I put together a PR for this.
The bug seems to be that mypy uses the outer return context to specialize a generic callable before it has checked whether the actual arguments still fit that specialized signature.
In OP's Vec example, the outer context pushes the method toward something like:
but the actual argument is still Vec[int]. So that contextual specialization is not a valid call. The same basic issue shows up in the recent min(..., key=...) example from @randolf-scholz : the return context can widen the item type enough that the key function no longer matches.
The PR keeps the existing contextual inference path, but adds a guard: if the context-specialized arguments are not compatible with the actual arguments, mypy can fall back to ordinary argument inference, as long as the ordinary inferred return type still satisfies the original outer context.
I had to keep this fairly narrow. In particular, constructors and special synthetic signatures keep the old behavior. Without that, tests like testAbstractTypeInADict failed, and some constructor/type-alias tests report worse Never-based messages.