From 7c5e74f01c82fe848133ba008c64e15efe8005b4 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 3 May 2026 23:34:03 -0700 Subject: [PATCH 1/2] Narrow match captures based on previous cases Fixes #16736 Closes #18155 --- mypy/checker.py | 36 +++++++++++++++-------------- test-data/unit/check-python310.test | 17 ++++++++++++++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 4d4f376f25dda..39c3e0c0f8566 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5960,7 +5960,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None: else_map[unwrapped_subject] = else_map[named_subject] pattern_map = self.propagate_up_typemap_info(pattern_map) else_map = self.propagate_up_typemap_info(else_map) - self.remove_capture_conflicts(pattern_type.captures, inferred_types) + self.check_and_remove_capture_conflicts(pattern_type.captures, inferred_types) self.push_type_map(pattern_map, from_assignment=False) if pattern_map: for expr, typ in pattern_map.items(): @@ -6066,15 +6066,8 @@ def infer_variable_types_from_type_maps( already_exists = True if isinstance(expr.node, Var) and expr.node.is_final: self.msg.cant_assign_to_final(expr.name, False, expr) - if self.check_subtype( - typ, - previous_type, - expr, - msg=message_registry.INCOMPATIBLE_TYPES_IN_CAPTURE, - subtype_label="pattern captures type", - supertype_label="variable has type", - ): - inferred_types[var] = previous_type + # We'll check compatibility in check_and_remove_capture_conflicts + inferred_types[var] = previous_type if not already_exists: new_type = UnionType.make_union(types) @@ -6086,15 +6079,24 @@ def infer_variable_types_from_type_maps( self.infer_variable_type(var, first_occurrence, new_type, first_occurrence) return inferred_types - def remove_capture_conflicts( + def check_and_remove_capture_conflicts( self, type_map: TypeMap, inferred_types: dict[SymbolNode, Type] ) -> None: - if not is_unreachable_map(type_map): - for expr, typ in list(type_map.items()): - if isinstance(expr, NameExpr): - node = expr.node - if node not in inferred_types or not is_subtype(typ, inferred_types[node]): - del type_map[expr] + if is_unreachable_map(type_map): + return + for expr, typ in list(type_map.items()): + if not isinstance(expr, NameExpr): + continue + node = expr.node + if node not in inferred_types or not self.check_subtype( + typ, + inferred_types[node], + expr, + msg=message_registry.INCOMPATIBLE_TYPES_IN_CAPTURE, + subtype_label="pattern captures type", + supertype_label="variable has type", + ): + del type_map[expr] def visit_type_alias_stmt(self, o: TypeAliasStmt) -> None: if o.alias_node: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 3f92d16ba46f3..242d97023a971 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1705,6 +1705,23 @@ reveal_type(a) # N: Revealed type is "builtins.bool" a = 3 reveal_type(a) # N: Revealed type is "builtins.int" +[case testMatchCapturePatternAfterPreviousCase] +# flags: --strict-equality --warn-unreachable + +def f1(x: int | None, y: int): + match x: + case None: + pass + case y: + reveal_type(y) # N: Revealed type is "builtins.int" + +def f2(x: int | None, y: int): + match x: + case None if bool(): + pass + case y: # E: Incompatible types in capture pattern (pattern captures type "int | None", variable has type "int") + reveal_type(y) # N: Revealed type is "builtins.int" + [case testMatchCapturePatternPreexistingIncompatible] # flags: --strict-equality --warn-unreachable a: str From da386f65bdb2138de2d756524e7e0ff046c2bf51 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 3 May 2026 23:46:39 -0700 Subject: [PATCH 2/2] test --- test-data/unit/check-python310.test | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 242d97023a971..03a0934b662c6 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1715,13 +1715,26 @@ def f1(x: int | None, y: int): case y: reveal_type(y) # N: Revealed type is "builtins.int" -def f2(x: int | None, y: int): +def f2(x: int | None, y: int, cond: bool): match x: - case None if bool(): + case None if cond: pass case y: # E: Incompatible types in capture pattern (pattern captures type "int | None", variable has type "int") reveal_type(y) # N: Revealed type is "builtins.int" +def f3(x: int | None, y: int): + match x: + case None if True: + pass + case y: + reveal_type(y) # N: Revealed type is "builtins.int" + + match x: + case None if False: + pass # E: Statement is unreachable + case y: # E: Incompatible types in capture pattern (pattern captures type "int | None", variable has type "int") + reveal_type(y) # N: Revealed type is "builtins.int" + [case testMatchCapturePatternPreexistingIncompatible] # flags: --strict-equality --warn-unreachable a: str