From 972f556f959b8c72de81e10d37493435b7286cf2 Mon Sep 17 00:00:00 2001 From: Mohit Kumar <91405114+mohitduhan19@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:55:21 +0530 Subject: [PATCH 1/2] Keep ad-hoc intersections when narrowing with a tuple of types --- mypy/checker.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6a0e51d00913..317675e31901 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8331,7 +8331,18 @@ def conditional_types_with_intersection( yes_type: Type | None = initial_types[0] no_type: Type | None = initial_types[1] - if not isinstance(get_proper_type(yes_type), UninhabitedType) or type_ranges is None: + if type_ranges is None: + return yes_type, no_type + + if not isinstance(get_proper_type(yes_type), UninhabitedType): + if yes_type is not None and len(type_ranges) > 1: + # Some items of a tuple of types (e.g. isinstance(x, (B, C))) may not + # overlap with expr_type at all, but a common subclass of expr_type + # and such an item could still match at runtime. Add ad-hoc + # intersections for those items, so that they are not dropped. + yes_type = self.add_intersections_for_disjoint_ranges( + expr_type, yes_type, type_ranges, consider_runtime_isinstance + ) return yes_type, no_type # If conditional_types was unable to successfully narrow the expr_type @@ -8374,6 +8385,51 @@ def conditional_types_with_intersection( new_yes_type = make_simplified_union(out) return new_yes_type, expr_type + def add_intersections_for_disjoint_ranges( + self, + expr_type: Type, + yes_type: Type, + type_ranges: list[TypeRange], + consider_runtime_isinstance: bool, + ) -> Type: + """Extend yes_type with intersections for type ranges disjoint from expr_type. + + This is used when narrowing to a tuple of types where some, but not all, + of the types overlap with expr_type. + """ + proper_type = get_proper_type(expr_type) + if isinstance(proper_type, UnionType): + possible_expr_types = get_proper_types(proper_type.relevant_items()) + else: + possible_expr_types = [proper_type] + if not all(isinstance(v, Instance) for v in possible_expr_types): + return yes_type + + items: list[Type] = [] + found_disjoint = False + for tr in type_ranges: + single_yes, _ = conditional_types( + expr_type, + [tr], + default=expr_type, + consider_runtime_isinstance=consider_runtime_isinstance, + ) + target = get_proper_type(tr.item) + if not isinstance(get_proper_type(single_yes), UninhabitedType) or not isinstance( + target, Instance + ): + items.append(single_yes) + continue + found_disjoint = True + for v in possible_expr_types: + assert isinstance(v, Instance) + intersection = self.intersect_instances((v, target), []) + if intersection is not None: + items.append(intersection) + if not found_disjoint: + return yes_type + return make_simplified_union(items) + def is_writable_attribute(self, node: Node) -> bool: """Check if an attribute is writable""" if isinstance(node, Var): From d0f82d806a5687c033433baabd3fe714c3775cdc Mon Sep 17 00:00:00 2001 From: Mohit Kumar <91405114+mohitduhan19@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:55:59 +0530 Subject: [PATCH 2/2] Add tests for isinstance narrowing with partially overlapping tuples --- test-data/unit/check-isinstance.test | 41 +++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index acd81839fcdc..cec91d2b802c 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2617,7 +2617,7 @@ def f(var: Union[int, str]) -> None: some_types = (str, tuple) another_type = list if isinstance(var, (*some_types, another_type)): - reveal_type(var) # N: Revealed type is "builtins.str" + reveal_type(var) # N: Revealed type is "__main__. | builtins.str" [builtins fixtures/isinstancelist.pyi] [case testIsInstanceWithWrongStarExpression] @@ -2926,6 +2926,45 @@ if isinstance(v3, (C, D)): reveal_type(v3) # N: Revealed type is "__main__. | __main__. | __main__. | __main__." [builtins fixtures/isinstance.pyi] +[case testIsInstanceAdHocIntersectionTuplePartialOverlap] +# flags: --warn-unreachable +from typing import Union +class A: pass +class B(A): pass +class C: pass +class D(A, C): pass + +def g(x: Union[B, C]) -> None: ... + +def f(x: A) -> None: + if isinstance(x, (B, C)): + reveal_type(x) # N: Revealed type is "__main__.B | __main__." + g(x) + else: + reveal_type(x) # N: Revealed type is "__main__.A" + if isinstance(x, (C, B)): + reveal_type(x) # N: Revealed type is "__main__. | __main__.B" + if isinstance(x, (B, D)): + reveal_type(x) # N: Revealed type is "__main__.B | __main__.D" + +def h(x: Union[A, int]) -> None: + if isinstance(x, (B, C)): + reveal_type(x) # N: Revealed type is "__main__.B | __main__. | __main__." +[builtins fixtures/isinstance.pyi] + +[case testIsInstanceAdHocIntersectionTuplePartialOverlapFinal] +# flags: --warn-unreachable +from typing import final +class A: pass +class B(A): pass +@final +class C: pass + +def f(x: A) -> None: + if isinstance(x, (B, C)): + reveal_type(x) # N: Revealed type is "__main__.B" +[builtins fixtures/isinstance.pyi] + [case testIsInstanceAdHocIntersectionSameNames] # flags: --warn-unreachable from foo import A as A2