From 37a16b891f32f5269ea940a4c51c6b43219f9455 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 24 Sep 2026 12:32:53 +0100 Subject: [PATCH 1/3] Try not simplifying unions during unification --- mypy/constraints.py | 4 ++-- mypy/subtypes.py | 3 +++ mypy/typestate.py | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 26986fa1975ab..304a8b1d90ba1 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -333,9 +333,9 @@ def _infer_constraints( # Type inference shouldn't be affected by whether union types have been simplified. # We however keep any ErasedType items, so that the caller will see it when using # checkexpr.has_erased_component(). - if isinstance(template, UnionType): + if not type_state.keep_unions and isinstance(template, UnionType): template = mypy.typeops.make_simplified_union(template.items, keep_erased=True) - if isinstance(actual, UnionType): + if not type_state.keep_unions and isinstance(actual, UnionType): actual = mypy.typeops.make_simplified_union(actual.items, keep_erased=True) # Ignore Any types from the type suggestion engine to avoid them diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a2f2929cbecd9..7f323b0be689d 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2165,6 +2165,8 @@ def unify_generic_callable( constraints: list[mypy.constraints.Constraint] = [] # There is some special logic for inference in callables, so better use them # as wholes instead of picking separate arguments. + keep_unions_old = type_state.keep_unions + type_state.keep_unions = True cs = mypy.constraints.infer_constraints( type.copy_modified(ret_type=UninhabitedType()), target.copy_modified(ret_type=UninhabitedType()), @@ -2177,6 +2179,7 @@ def unify_generic_callable( type.ret_type, target.ret_type, return_constraint_direction ) constraints.extend(c) + type_state.keep_unions = keep_unions_old inferred_vars, _ = mypy.solve.solve_constraints( type.variables, constraints, allow_polymorphic=True ) diff --git a/mypy/typestate.py b/mypy/typestate.py index d45837dad645a..c99e3bfc3fcbc 100644 --- a/mypy/typestate.py +++ b/mypy/typestate.py @@ -113,6 +113,7 @@ def __init__(self) -> None: self.inferring = [] self.infer_unions = False self.infer_polymorphic = False + self.keep_unions = False def is_assumed_subtype(self, left: Type, right: Type) -> bool: for l, r in reversed(self._assuming): From bf6b7c7828fd1bec0be9ceda5f95f12d9f73a622 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 24 Sep 2026 13:44:49 +0100 Subject: [PATCH 2/3] Add some comments --- mypy/subtypes.py | 6 ++++-- mypy/typestate.py | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 7f323b0be689d..6704bb80147d8 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2163,10 +2163,12 @@ def unify_generic_callable( return_constraint_direction = mypy.constraints.SUBTYPE_OF constraints: list[mypy.constraints.Constraint] = [] - # There is some special logic for inference in callables, so better use them - # as wholes instead of picking separate arguments. + # This code is hot in numerical libraries, so we use a faster inference + # algorithm (that may give slightly worse results in rare cases). keep_unions_old = type_state.keep_unions type_state.keep_unions = True + # There is some special logic for inference in callables, so better use them + # as wholes instead of picking separate arguments. cs = mypy.constraints.infer_constraints( type.copy_modified(ret_type=UninhabitedType()), target.copy_modified(ret_type=UninhabitedType()), diff --git a/mypy/typestate.py b/mypy/typestate.py index c99e3bfc3fcbc..f820288129cc0 100644 --- a/mypy/typestate.py +++ b/mypy/typestate.py @@ -95,8 +95,13 @@ class TypeState: # Whether to use new type inference algorithm that can infer polymorphic types. # This is temporary and will be removed soon when new algorithm is more polished. infer_polymorphic: bool + # No not simplify/normalize unions during type inference in this flag is set. + # Technically, union simplification gives strictly better type inference, but it + # may have bad performance (since it is quadratic). So we may want to disable + # it situations where performance is important. + keep_unions: bool - # N.B: We do all of the accesses to these properties through + # N.B: We do all the accesses to these properties through # TypeState, instead of making these classmethods and accessing # via the cls parameter, since mypyc can optimize accesses to # Final attributes of a directly referenced type. From e9219b26dfb4258d4366ff92b6cb442f838b072e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 25 Sep 2026 00:21:14 +0100 Subject: [PATCH 3/3] try/finally --- mypy/subtypes.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 37354c02402c2..bda067a144832 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2165,26 +2165,26 @@ def unify_generic_callable( if return_constraint_direction is None: return_constraint_direction = mypy.constraints.SUBTYPE_OF - constraints: list[mypy.constraints.Constraint] = [] # This code is hot in numerical libraries, so we use a faster inference # algorithm (that may give slightly worse results in rare cases). keep_unions_old = type_state.keep_unions type_state.keep_unions = True - # There is some special logic for inference in callables, so better use them - # as wholes instead of picking separate arguments. - cs = mypy.constraints.infer_constraints( - type.copy_modified(ret_type=UninhabitedType()), - target.copy_modified(ret_type=UninhabitedType()), - mypy.constraints.SUBTYPE_OF, - skip_neg_op=True, - ) - constraints.extend(cs) - if not ignore_return: - c = mypy.constraints.infer_constraints( - type.ret_type, target.ret_type, return_constraint_direction + try: + # There is some special logic for inference in callables, so better use + # them as wholes instead of picking separate arguments. + constraints = mypy.constraints.infer_constraints( + type.copy_modified(ret_type=UninhabitedType()), + target.copy_modified(ret_type=UninhabitedType()), + mypy.constraints.SUBTYPE_OF, + skip_neg_op=True, ) - constraints.extend(c) - type_state.keep_unions = keep_unions_old + if not ignore_return: + c = mypy.constraints.infer_constraints( + type.ret_type, target.ret_type, return_constraint_direction + ) + constraints.extend(c) + finally: + type_state.keep_unions = keep_unions_old inferred_vars, _ = mypy.solve.solve_constraints( type.variables, constraints, allow_polymorphic=True )