From 7879d75f8040c4689571eb49f2757614e890dfa9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 28 Sep 2026 17:06:53 +0100 Subject: [PATCH 01/14] Some experiments --- mypy/checkexpr.py | 47 +++++++++++++++++++++++++++++++++++++---------- mypy/infer.py | 10 +++++++--- mypy/typeops.py | 4 ++-- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8b40fbb592789..c65ced2111e36 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -19,6 +19,7 @@ from mypy.checkmember import analyze_member_access, has_operator from mypy.checkstrformat import StringFormatterChecker from mypy.constant_fold import constant_fold_expr +from mypy.constraints import Constraint from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error from mypy.expandtype import ( @@ -1792,8 +1793,10 @@ def check_callable_call( callee = freshen_function_type_vars(callee) self.freshen_cache[(context, original_callee)] = callee + fresh_callee = callee + constraints: list[Constraint] = [] if callee.is_generic(): - callee = self.infer_function_type_arguments_using_context(callee, context) + callee, constraints = self.infer_function_type_arguments_using_context(callee, context) formal_to_actual = map_actuals_to_formals( arg_kinds, @@ -1825,12 +1828,12 @@ def check_callable_call( ) callee = callee.copy_modified(arg_types=[new_arg_type]) - if callee.is_generic(): + if fresh_callee.is_generic(): need_refresh = any( - isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in callee.variables + isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in fresh_callee.variables ) callee = self.infer_function_type_arguments( - callee, args, arg_kinds, arg_names, formal_to_actual, need_refresh, context + callee, fresh_callee, constraints, args, arg_kinds, arg_names, formal_to_actual, need_refresh, context ) if need_refresh: # Argument kinds etc. may have changed due to @@ -2113,7 +2116,7 @@ def infer_arg_types_in_context( def infer_function_type_arguments_using_context( self, callable: CallableType, error_context: Context - ) -> CallableType: + ) -> tuple[CallableType, list[Constraint]]: """Unify callable return type to type context to infer type vars. For example, if the return type is set[t] where 't' is a type variable @@ -2122,7 +2125,7 @@ def infer_function_type_arguments_using_context( """ ctx = self.type_context[-1] if not ctx: - return callable + return callable, [] # The return type may have references to type metavariables that # we are inferring right now. We must consider them as indeterminate # and they are not potential results; thus we replace them with the @@ -2179,8 +2182,8 @@ def infer_function_type_arguments_using_context( # TODO: we may want to add similar exception if all arguments are lambdas, since # in this case external context is almost everything we have. if not is_generic_instance(ctx) and not is_literal_type_like(ctx): - return callable.copy_modified() - args = infer_type_arguments( + return callable.copy_modified(), [] + args, constraints = infer_type_arguments( callable.variables, ret_type, erased_ctx, skip_unsatisfied=True ) # Only substitute non-Uninhabited and non-erased types. @@ -2194,11 +2197,13 @@ def infer_function_type_arguments_using_context( # We will use argument context to infer more variables. return self.apply_generic_arguments( callable, new_args, error_context, skip_unsatisfied=True - ) + ), constraints def infer_function_type_arguments( self, callee_type: CallableType, + fresh_callee: CallableType, + ret_constraints: list[Constraint], args: list[Expression], arg_kinds: list[ArgKind], arg_names: Sequence[str | None] | None, @@ -2234,15 +2239,35 @@ def infer_function_type_arguments( pass1_args.append(arg) inferred_args, _ = infer_function_type_arguments( - callee_type, + fresh_callee, pass1_args, arg_kinds, arg_names, formal_to_actual, context=self.argument_infer_context(), strict=self.chk.in_checked_function(), + extra_constraints=ret_constraints, ) + if not any( + a is None + or isinstance(get_proper_type(a), UninhabitedType) + or has_erased_component(a) + or set(get_type_vars(a)) & set(callee_type.variables) + for a in inferred_args + ): + return self.apply_inferred_arguments(fresh_callee, inferred_args, context) + else: + inferred_args, _ = infer_function_type_arguments( + callee_type, + pass1_args, + arg_kinds, + arg_names, + formal_to_actual, + context=self.argument_infer_context(), + strict=self.chk.in_checked_function(), + ) + partially_applied = False if 2 in arg_pass_nums: # Second pass of type inference. callee_type, inferred_args = self.infer_function_type_arguments_pass2( @@ -2255,6 +2280,7 @@ def infer_function_type_arguments( need_refresh, context, ) + partially_applied = True if ( callee_type.special_sig == "dict" @@ -2303,6 +2329,7 @@ def infer_function_type_arguments( context=self.argument_infer_context(), strict=self.chk.in_checked_function(), allow_polymorphic=True, + # extra_constraints=ret_constraints if not partially_applied else None, ) poly_callee_type = self.apply_generic_arguments( callee_type, poly_inferred_args, context diff --git a/mypy/infer.py b/mypy/infer.py index 2c155ee2456b3..a07a152eb0755 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -9,7 +9,7 @@ SUBTYPE_OF, SUPERTYPE_OF, infer_constraints, - infer_constraints_for_callable, + infer_constraints_for_callable, Constraint, ) from mypy.nodes import ArgKind from mypy.solve import solve_constraints @@ -40,6 +40,7 @@ def infer_function_type_arguments( context: ArgumentInferContext, strict: bool = True, allow_polymorphic: bool = False, + extra_constraints: list[Constraint] | None = None, ) -> tuple[list[Type | None], list[TypeVarLikeType]]: """Infer the type arguments of a generic function. @@ -58,6 +59,8 @@ def infer_function_type_arguments( constraints = infer_constraints_for_callable( callee_type, arg_types, arg_kinds, arg_names, formal_to_actual, context ) + if extra_constraints: + constraints += extra_constraints # Solve constraints. type_vars = callee_type.variables @@ -71,10 +74,11 @@ def infer_type_arguments( is_supertype: bool = False, skip_unsatisfied: bool = False, erase_types: bool = True, -) -> list[Type | None]: +) -> tuple[list[Type | None], list[Constraint]]: # Like infer_function_type_arguments, but only match a single type # against a generic type. constraints = infer_constraints( template, actual, SUPERTYPE_OF if is_supertype else SUBTYPE_OF, erase_types=erase_types ) - return solve_constraints(type_vars, constraints, skip_unsatisfied=skip_unsatisfied)[0] + solutions = solve_constraints(type_vars, constraints, skip_unsatisfied=skip_unsatisfied)[0] + return solutions, constraints diff --git a/mypy/typeops.py b/mypy/typeops.py index eb9d0d1474b8b..d3e2ad9a7e311 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -552,7 +552,7 @@ def bind_self_inner( self_vars = [tv for tv in func.variables if tv.id in self_ids] # Solve for these type arguments using the actual class or instance type. - typeargs = infer_type_arguments( + typeargs, _ = infer_type_arguments( self_vars, self_param_type, original_type, is_supertype=True, erase_types=False ) if ( @@ -561,7 +561,7 @@ def bind_self_inner( and isinstance(original_type, (Instance, TypeVarType, TupleType)) ): # In case we call a classmethod through an instance x, fallback to type(x). - typeargs = infer_type_arguments( + typeargs, _ = infer_type_arguments( self_vars, self_param_type, TypeType(original_type), From 672cc5a08b28b3d6659b902707389c0d8c801a9e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 13 Oct 2026 16:08:52 +0100 Subject: [PATCH 02/14] Some more experiments --- mypy/checkexpr.py | 35 +++++++++++++++++++++++------------ mypy/infer.py | 3 ++- mypy/solve.py | 1 + 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c65ced2111e36..c1f369e6a3963 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1833,7 +1833,15 @@ def check_callable_call( isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in fresh_callee.variables ) callee = self.infer_function_type_arguments( - callee, fresh_callee, constraints, args, arg_kinds, arg_names, formal_to_actual, need_refresh, context + callee, + fresh_callee, + constraints, + args, + arg_kinds, + arg_names, + formal_to_actual, + need_refresh, + context, ) if need_refresh: # Argument kinds etc. may have changed due to @@ -2195,9 +2203,10 @@ def infer_function_type_arguments_using_context( new_args.append(arg) # Don't show errors after we have only used the outer context for inference. # We will use argument context to infer more variables. - return self.apply_generic_arguments( - callable, new_args, error_context, skip_unsatisfied=True - ), constraints + return ( + self.apply_generic_arguments(callable, new_args, error_context, skip_unsatisfied=True), + constraints, + ) def infer_function_type_arguments( self, @@ -2248,12 +2257,16 @@ def infer_function_type_arguments( strict=self.chk.in_checked_function(), extra_constraints=ret_constraints, ) - if not any( - a is None - or isinstance(get_proper_type(a), UninhabitedType) - or has_erased_component(a) - or set(get_type_vars(a)) & set(callee_type.variables) - for a in inferred_args + if ( + not any( + a is None + or isinstance(get_proper_type(a), UninhabitedType) + or has_erased_component(a) + or set(get_type_vars(a)) & set(callee_type.variables) + for a in inferred_args + ) + and fresh_callee.special_sig is None + and not fresh_callee.is_type_obj() ): return self.apply_inferred_arguments(fresh_callee, inferred_args, context) else: @@ -2267,7 +2280,6 @@ def infer_function_type_arguments( strict=self.chk.in_checked_function(), ) - partially_applied = False if 2 in arg_pass_nums: # Second pass of type inference. callee_type, inferred_args = self.infer_function_type_arguments_pass2( @@ -2280,7 +2292,6 @@ def infer_function_type_arguments( need_refresh, context, ) - partially_applied = True if ( callee_type.special_sig == "dict" diff --git a/mypy/infer.py b/mypy/infer.py index a07a152eb0755..cf4c77009d50a 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -8,8 +8,9 @@ from mypy.constraints import ( SUBTYPE_OF, SUPERTYPE_OF, + Constraint, infer_constraints, - infer_constraints_for_callable, Constraint, + infer_constraints_for_callable, ) from mypy.nodes import ArgKind from mypy.solve import solve_constraints diff --git a/mypy/solve.py b/mypy/solve.py index 4a5eec47ca60d..6bdf262a5ed4b 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -302,6 +302,7 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: if sorted_lowers: bottom = join_type_list(sorted_lowers) + top = bottom for target in uppers: if top is None: top = target From c8b6c7525752defd59249dd0cae281214458f32a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Oct 2026 01:45:37 +0100 Subject: [PATCH 03/14] Re-shuffle --- mypy/checkexpr.py | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c1f369e6a3963..6d6a27be4685a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2247,38 +2247,38 @@ def infer_function_type_arguments( else: pass1_args.append(arg) - inferred_args, _ = infer_function_type_arguments( - fresh_callee, - pass1_args, - arg_kinds, - arg_names, - formal_to_actual, - context=self.argument_infer_context(), - strict=self.chk.in_checked_function(), - extra_constraints=ret_constraints, - ) - if ( - not any( - a is None - or isinstance(get_proper_type(a), UninhabitedType) - or has_erased_component(a) - or set(get_type_vars(a)) & set(callee_type.variables) - for a in inferred_args - ) - and fresh_callee.special_sig is None - and not fresh_callee.is_type_obj() - ): - return self.apply_inferred_arguments(fresh_callee, inferred_args, context) - else: + if fresh_callee.special_sig is None and not fresh_callee.is_type_obj(): inferred_args, _ = infer_function_type_arguments( - callee_type, + fresh_callee, pass1_args, arg_kinds, arg_names, formal_to_actual, context=self.argument_infer_context(), strict=self.chk.in_checked_function(), + extra_constraints=ret_constraints, ) + if not any( + a is None + or isinstance(get_proper_type(a), UninhabitedType) + or has_erased_component(a) + or set(get_type_vars(a)) & set(callee_type.variables) + for a in inferred_args + ): + return self.apply_inferred_arguments(fresh_callee, inferred_args, context) + + if not callee_type.is_generic(): + return callee_type + + inferred_args, _ = infer_function_type_arguments( + callee_type, + pass1_args, + arg_kinds, + arg_names, + formal_to_actual, + context=self.argument_infer_context(), + strict=self.chk.in_checked_function(), + ) if 2 in arg_pass_nums: # Second pass of type inference. From e710f71dbec92b6c2e84c77c3171cbe66363b2a2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Oct 2026 01:55:48 +0100 Subject: [PATCH 04/14] One more guard --- mypy/checkexpr.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 6d6a27be4685a..d25e8ba06111f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2247,7 +2247,11 @@ def infer_function_type_arguments( else: pass1_args.append(arg) - if fresh_callee.special_sig is None and not fresh_callee.is_type_obj(): + if ( + fresh_callee != callee_type + and fresh_callee.special_sig is None + and not fresh_callee.is_type_obj() + ): inferred_args, _ = infer_function_type_arguments( fresh_callee, pass1_args, From 1c02b02f63bc5e460e95c3bd695149ac404bd22f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Oct 2026 13:20:44 +0100 Subject: [PATCH 05/14] Perf + random --- mypy/checkexpr.py | 2 ++ mypy/constraints.py | 8 +++++++- mypy/messages.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d25e8ba06111f..497cbbcf868ad 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2201,6 +2201,8 @@ def infer_function_type_arguments_using_context( new_args.append(None) else: new_args.append(arg) + if all(a is None for a in new_args): + return callable, constraints # Don't show errors after we have only used the outer context for inference. # We will use argument context to infer more variables. return ( diff --git a/mypy/constraints.py b/mypy/constraints.py index e54983c9744f9..5e4dee28ab0a5 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -311,7 +311,11 @@ def infer_constraints( for (t, a) in reversed(type_state.inferring) ): return [] - if has_recursive_types(template) or isinstance(get_proper_type(template), Instance): + if ( + isinstance(template, TypeAliasType) + and template.is_recursive + or isinstance(get_proper_type(template), Instance) + ): # This case requires special care because it may cause infinite recursion. # Note that we include Instances because the may be recursive as str(Sequence[str]). if not has_type_vars(template): @@ -529,6 +533,8 @@ def handle_recursive_union(template: UnionType, actual: Type, direction: int) -> # the union in two parts, and try inferring sequentially. non_type_var_items = [t for t in template.items if not isinstance(t, TypeVarType)] type_var_items = [t for t in template.items if isinstance(t, TypeVarType)] + if not type_var_items or not non_type_var_items: + return [] return infer_constraints( UnionType.make_union(non_type_var_items), actual, direction ) or infer_constraints(UnionType.make_union(type_var_items), actual, direction) diff --git a/mypy/messages.py b/mypy/messages.py index 0f3a5be368a4b..8b4c1354e4f4a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -3129,7 +3129,7 @@ def [T <: int] f(self, x: int, y: T) -> None # TODO: avoid different logic for incremental runs. first_arg = get_first_arg(tp) - if tp.is_type_obj(): + if tp.is_type_obj() or not tp.is_bound: skip_self = True if first_arg and not skip_self: if s: From 3d1ccdf79e804eeb1541b6b5722485497f17f576 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Oct 2026 14:27:26 +0100 Subject: [PATCH 06/14] Postpone solver change; fix tests --- mypy/solve.py | 1 - test-data/unit/check-parameter-specification.test | 4 ++-- test-data/unit/check-recursive-types.test | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mypy/solve.py b/mypy/solve.py index 6bdf262a5ed4b..4a5eec47ca60d 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -302,7 +302,6 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: if sorted_lowers: bottom = join_type_list(sorted_lowers) - top = bottom for target in uppers: if top is None: top = target diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 2441770b5598e..703899caea1cb 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2814,6 +2814,6 @@ x: Union[Any, C[[Any, Any]]] def test(x: int) -> None: ... -x = foo(test) # E: Argument 1 to "foo" has incompatible type "Callable[[int], None]"; expected "Callable[[Any, Any], None]" -reveal_type(x) # N: Revealed type is "__main__.C[[Any, Any]]" +x = foo(test) +reveal_type(x) # N: Revealed type is "__main__.C[Any] | __main__.C[[Any, Any]]" [builtins fixtures/paramspec.pyi] diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 5d81e76ba8c6e..437e1f367f371 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -298,8 +298,8 @@ a = [[b]] # OK b = aa # E: Incompatible types in assignment (expression has type "Nested[A]", variable has type "B") def join(a: T, b: T) -> T: ... -reveal_type(join(a, b)) # N: Revealed type is "typing.Sequence[__main__.A | typing.Sequence[__main__.A | ...]]" -reveal_type(join(b, a)) # N: Revealed type is "typing.Sequence[__main__.A | typing.Sequence[__main__.A | ...]]" +reveal_type(join(aa, b)) # N: Revealed type is "typing.Sequence[__main__.A | typing.Sequence[__main__.A | ...]]" +reveal_type(join(b, aa)) # N: Revealed type is "typing.Sequence[__main__.A | typing.Sequence[__main__.A | ...]]" [builtins fixtures/isinstancelist.pyi] [case testRecursiveAliasWithRecursiveInstanceInference] From 3c52b8e4e088045f37cb3c2ca599f914e960139f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Oct 2026 14:28:31 +0100 Subject: [PATCH 07/14] Cleanup --- mypy/checkexpr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 497cbbcf868ad..19868f5b9c4dd 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2346,7 +2346,6 @@ def infer_function_type_arguments( context=self.argument_infer_context(), strict=self.chk.in_checked_function(), allow_polymorphic=True, - # extra_constraints=ret_constraints if not partially_applied else None, ) poly_callee_type = self.apply_generic_arguments( callee_type, poly_inferred_args, context From b71998fa1b6de17256951e7d32fd4e1ff5018357 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 21 Oct 2026 21:53:03 +0100 Subject: [PATCH 08/14] Tune expression cache --- mypy/checkexpr.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 19868f5b9c4dd..3f9c1361c5047 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6330,7 +6330,11 @@ def accept( # context, and use enclosing one, see infer_lambda_type_using_context(). # TODO: consider using cache for more expression kinds. elif ( - isinstance(node, (CallExpr, ListExpr, TupleExpr, DictExpr, OpExpr, MemberExpr)) + ( + isinstance(node, (CallExpr, ListExpr, TupleExpr, DictExpr, OpExpr)) + or isinstance(node, MemberExpr) + and node.node is None + ) and not (self.in_lambda_expr or self.chk.current_node_deferred) and not self.chk.options.disable_expression_cache ): From 0204991ee85687769d53f4d45f416a661c47ed32 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 23 Oct 2026 14:07:58 +0100 Subject: [PATCH 09/14] Postpone low-pri optimization --- mypy/checkexpr.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3f9c1361c5047..4ff1d9ba7069d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2201,8 +2201,6 @@ def infer_function_type_arguments_using_context( new_args.append(None) else: new_args.append(arg) - if all(a is None for a in new_args): - return callable, constraints # Don't show errors after we have only used the outer context for inference. # We will use argument context to infer more variables. return ( From e3879a7ccc294ae6567b7d6289aad573799eb4c3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 23 Oct 2026 14:36:16 +0100 Subject: [PATCH 10/14] Cosmetics --- mypy/checkexpr.py | 32 +++++++++++++++++++++----------- mypy/constraints.py | 2 ++ mypy/infer.py | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4ff1d9ba7069d..676210a60c044 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1793,7 +1793,7 @@ def check_callable_call( callee = freshen_function_type_vars(callee) self.freshen_cache[(context, original_callee)] = callee - fresh_callee = callee + initial_callee = callee constraints: list[Constraint] = [] if callee.is_generic(): callee, constraints = self.infer_function_type_arguments_using_context(callee, context) @@ -1828,13 +1828,13 @@ def check_callable_call( ) callee = callee.copy_modified(arg_types=[new_arg_type]) - if fresh_callee.is_generic(): + if initial_callee.is_generic(): need_refresh = any( - isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in fresh_callee.variables + isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in callee.variables ) callee = self.infer_function_type_arguments( callee, - fresh_callee, + initial_callee, constraints, args, arg_kinds, @@ -2211,7 +2211,7 @@ def infer_function_type_arguments_using_context( def infer_function_type_arguments( self, callee_type: CallableType, - fresh_callee: CallableType, + initial_callee_type: CallableType, ret_constraints: list[Constraint], args: list[Expression], arg_kinds: list[ArgKind], @@ -2222,7 +2222,8 @@ def infer_function_type_arguments( ) -> CallableType: """Infer the type arguments for a generic callee type. - Infer based on the types of arguments. + Infer based on the types of arguments. The `initial_callee_type` is callee + type before applying arguments inferred from return type context. Return a derived callable type that has the arguments applied. """ @@ -2247,13 +2248,16 @@ def infer_function_type_arguments( else: pass1_args.append(arg) + # First, try inferring callee type using both argument type and return + # constraints. Do not do this if special_sig is set or for type objects, + # those are special-cased elsewhere. if ( - fresh_callee != callee_type - and fresh_callee.special_sig is None - and not fresh_callee.is_type_obj() + initial_callee_type != callee_type + and initial_callee_type.special_sig is None + and not initial_callee_type.is_type_obj() ): inferred_args, _ = infer_function_type_arguments( - fresh_callee, + initial_callee_type, pass1_args, arg_kinds, arg_names, @@ -2262,6 +2266,8 @@ def infer_function_type_arguments( strict=self.chk.in_checked_function(), extra_constraints=ret_constraints, ) + # Only use two-side inference if it is fully successful, + # otherwise, fallback to return type context as a tiebreaker. if not any( a is None or isinstance(get_proper_type(a), UninhabitedType) @@ -2269,9 +2275,12 @@ def infer_function_type_arguments( or set(get_type_vars(a)) & set(callee_type.variables) for a in inferred_args ): - return self.apply_inferred_arguments(fresh_callee, inferred_args, context) + return self.apply_inferred_arguments( + initial_callee_type, inferred_args, context + ) if not callee_type.is_generic(): + # Fast path: return context inferred all type arguments. return callee_type inferred_args, _ = infer_function_type_arguments( @@ -6330,6 +6339,7 @@ def accept( elif ( ( isinstance(node, (CallExpr, ListExpr, TupleExpr, DictExpr, OpExpr)) + # Only cache non-static access, it may be slow, and may freshen type vars. or isinstance(node, MemberExpr) and node.node is None ) diff --git a/mypy/constraints.py b/mypy/constraints.py index 5e4dee28ab0a5..aa095316c234e 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -311,6 +311,8 @@ def infer_constraints( for (t, a) in reversed(type_state.inferring) ): return [] + # This guard matches the one we use for subtyping, it is slightly more risky, + # but it is much faster than has_recursive_types(). if ( isinstance(template, TypeAliasType) and template.is_recursive diff --git a/mypy/infer.py b/mypy/infer.py index cf4c77009d50a..d514900e66d3b 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -81,5 +81,5 @@ def infer_type_arguments( constraints = infer_constraints( template, actual, SUPERTYPE_OF if is_supertype else SUBTYPE_OF, erase_types=erase_types ) - solutions = solve_constraints(type_vars, constraints, skip_unsatisfied=skip_unsatisfied)[0] + solutions, _ = solve_constraints(type_vars, constraints, skip_unsatisfied=skip_unsatisfied) return solutions, constraints From 1cacab75d0339cc7c16d4de439bede4e10306fb5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 23 Oct 2026 15:38:45 +0100 Subject: [PATCH 11/14] Add some tests --- test-data/unit/check-inference-context.test | 45 +++++++++++++++++++++ test-data/unit/check-python313.test | 23 +++++++++++ 2 files changed, 68 insertions(+) diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 3735f0183eb4a..01eed498736ea 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1629,3 +1629,48 @@ def foo(args: Union[Iterable[Union[str, int]], str, int]) -> Iterable[str]: ) return args [builtins fixtures/isinstancelist.pyi] + +[case testInferenceContextBothSidesInvariant] +from typing import TypeVar, Generic + +T = TypeVar("T") + +class B(Generic[T]): ... +class C(B[int]): ... + +def first(xs: list[T]) -> T: ... + +b: B[int] +cs: list[C] +b = first(cs) # OK +reveal_type(b) # N: Revealed type is "__main__.C" + +[case testInferenceContextBothSidesNarrow] +from typing import TypeVar, Generic + +T = TypeVar("T") + +class B(Generic[T]): ... +class C(B[int]): ... + +def same(xs: T) -> T: ... + +b: B[int] +b = same(C()) # This should narrow +reveal_type(b) # N: Revealed type is "__main__.C" + +[case testInferenceContextBothSidesJoin] +from typing import TypeVar, Generic, Callable, Union + +T = TypeVar("T") + +class A: ... +class B: ... +class C: ... + +def make(x: T) -> Callable[[T], None]: ... + +x: Union[B, C] +fn: Callable[[Union[A, B]], None] +fn = make(x) +reveal_type(fn) # N: Revealed type is "def (__main__.B | __main__.C | __main__.A)" diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index d52cb575bc916..7af7a55e73f39 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -480,3 +480,26 @@ reveal_type(x) # N: Revealed type is "builtins.list[tuple[()]]" reveal_type(y) # N: Revealed type is "builtins.list[tuple[()]]" reveal_type(z) # N: Revealed type is "builtins.list[tuple[()]]" [builtins fixtures/tuple.pyi] + +[case testInferenceContextBothSidesSlack] +from typing import Never + +class Vec[T]: # invariant in T + def _items(self) -> list[T]: ... + def _append(self, item: T) -> None: ... + +def concat_vecs[T1, T2, T_slack=Never]( + left: Vec[T1], + right: Vec[T2], +) -> Vec[T1 | T2 | T_slack]: + return Vec() + +class A: ... +class B: ... +class C: ... + +def test_vec_concat(left: Vec[A], right: Vec[B]) -> None: + reveal_type(concat_vecs(left, right)) # N: Revealed type is "__main__.Vec[__main__.A | __main__.B]" + _0: Vec[A | B] = concat_vecs(left, right) # OK + _1: Vec[A | B | C] = concat_vecs(left, right) # OK + _2: Vec[object] = concat_vecs(left, right) # OK From 77162e054393907f0397e106b66514b6522e431b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 24 Oct 2026 01:37:13 +0100 Subject: [PATCH 12/14] Fix refresh; show speed regression --- .github/workflows/mypy_primer.yml | 1 + mypy/checkexpr.py | 2 +- test-data/unit/check-parameter-specification.test | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mypy_primer.yml b/.github/workflows/mypy_primer.yml index 654cdf2c5cf11..c4a64c4a9696d 100644 --- a/.github/workflows/mypy_primer.yml +++ b/.github/workflows/mypy_primer.yml @@ -67,6 +67,7 @@ jobs: --debug \ --additional-flags="--debug-serialize" \ --output concise \ + --show-speed-regression \ --mypy-install-librt \ | tee diff_${{ matrix.shard-index }}.txt ) || [ $? -eq 1 ] diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 676210a60c044..0bd769699ad5f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1830,7 +1830,7 @@ def check_callable_call( if initial_callee.is_generic(): need_refresh = any( - isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in callee.variables + isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in initial_callee.variables ) callee = self.infer_function_type_arguments( callee, diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 703899caea1cb..0e3d5ab451172 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2817,3 +2817,17 @@ def test(x: int) -> None: ... x = foo(test) reveal_type(x) # N: Revealed type is "__main__.C[Any] | __main__.C[[Any, Any]]" [builtins fixtures/paramspec.pyi] + +[case testTwoSidedInferenceWithParamSpec] +from typing import Callable, Generic, TypeVar +from typing_extensions import ParamSpec + +P = ParamSpec("P") +T = TypeVar("T") +class C(Generic[P, T]): ... + +def thing(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> C[P, T]: ... + +def test(x: int = 0) -> None: ... +f: C[[], None] = thing(test) +[builtins fixtures/paramspec.pyi] From b323e3afb14064d2fd595e72fa7fbe081a99b360 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 24 Oct 2026 13:08:55 +0100 Subject: [PATCH 13/14] Fix type object join --- mypy/join.py | 10 ++++++++-- test-data/unit/check-functions.test | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/mypy/join.py b/mypy/join.py index c609b303bb087..b52eb8a6f69e0 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -809,8 +809,11 @@ def join_similar_callables(t: CallableType, s: CallableType) -> CallableType: # The result should always use 'function' as a fallback if either operands are using it. if t.fallback.type.fullname == "builtins.function": fallback = t.fallback - else: + elif s.fallback.type.fullname == "builtins.function": fallback = s.fallback + else: + fallback = InstanceJoiner().join_instances(t.fallback, s.fallback) + assert isinstance(fallback, Instance) instance_type = None if t.instance_type is not None and s.instance_type is not None: instance_type = join_types(t.instance_type, s.instance_type) @@ -867,8 +870,11 @@ def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType: # The result should always use 'function' as a fallback if either operands are using it. if t.fallback.type.fullname == "builtins.function": fallback = t.fallback - else: + elif s.fallback.type.fullname == "builtins.function": fallback = s.fallback + else: + fallback = InstanceJoiner().join_instances(t.fallback, s.fallback) + assert isinstance(fallback, Instance) instance_type = None if t.instance_type is not None and s.instance_type is not None: instance_type = join_types(t.instance_type, s.instance_type) diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 445b5ce26ee4a..d665476b907e4 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3957,3 +3957,26 @@ Ex = TypeVar("Ex", covariant=True) def takes_lambda(func: Callable[[Ex], Any]) -> None: pass + +[case testTypeObjectJoinWithOverloadsCorrectFallback] +from typing import Iterable, overload + +class M(type): ... +class A(metaclass=M): ... +class B: ... +class C(metaclass=M): + @overload + def __init__(self, x: int) -> None: ... + @overload + def __init__(self, x: str) -> None: ... + def __init__(self, x): + pass + + +def f(x: Iterable[object]) -> None: ... +f([A, B, C]) +f([B, A, C]) +f([B, C, A]) +f([A, C, B]) +f([C, A, B]) +f([C, B, A]) From 03ca213a967278452e83ef59d520a715f117c965 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 24 Oct 2026 17:22:14 +0100 Subject: [PATCH 14/14] Keep old logic for recursive types --- mypy/checkexpr.py | 3 +++ test-data/unit/check-recursive-types.test | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0bd769699ad5f..af9f7671270c2 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2255,6 +2255,9 @@ def infer_function_type_arguments( initial_callee_type != callee_type and initial_callee_type.special_sig is None and not initial_callee_type.is_type_obj() + # This is not very principled, but also prefer outer context for + # recursive types (because values can never be recursive). + and not type_state.infer_unions ): inferred_args, _ = infer_function_type_arguments( initial_callee_type, diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 437e1f367f371..81d1b5dacca59 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -15,6 +15,15 @@ class Bad: ... x = ["foo", {"bar": [Bad()]}] # E: List item 0 has incompatible type "Bad"; expected "str | list[JSON] | dict[str, JSON]" [builtins fixtures/isinstancelist.pyi] +[case testRecursiveAliasNestedWithAny] +from typing import Any, Dict, List, Union + +JSON = Union[str, List[JSON], Dict[str, JSON]] + +a: Any +x: JSON = {"str": [{"bar": a}]} +[builtins fixtures/isinstancelist.pyi] + [case testRecursiveAliasBasicGenericSubtype] from typing import Union, TypeVar, Sequence, List