From ec334c42d22966f01470f54543bbfc7f8253491b Mon Sep 17 00:00:00 2001 From: traveltamers Date: Thu, 2 Apr 2026 12:17:49 -0400 Subject: [PATCH 1/4] fix(narrowing): add _collect_walrus_type_map and call it in `and` branch Add _collect_walrus_type_map to TypeChecker that recursively walks arbitrary expression trees collecting AssignmentExpr type narrowings. Call it in find_isinstance_check_helper's "and" branch to register walrus assignments from the right operand into the true-branch type map. --- mypy/checker.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 754a407331ed..a50312c582e0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5,7 +5,7 @@ import itertools from collections import defaultdict from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence, Set as AbstractSet -from contextlib import ExitStack, contextmanager +from contextlib importh ExitStack, contextmanager from typing import ( Final, Generic, @@ -6497,6 +6497,10 @@ def find_isinstance_check_helper( elif isinstance(node, OpExpr) and node.op == "and": left_if_vars, left_else_vars = self.find_isinstance_check(node.left) right_if_vars, right_else_vars = self.find_isinstance_check(node.right) + # Collect type narrowings from any walrus assignments nested in the + # right operand. In the true branch of (A and B), all walrus assignments + # in B are guaranteed to have executed, so we can narrow their targets. + self._collect_walrus_type_map(node.right, right_if_vars) # (e1 and e2) is true if both e1 and e2 are true, # and false if at least one of e1 and e2 is false. @@ -7123,6 +7127,40 @@ def _propagate_walrus_assignments( return parent_expr return expr + def _collect_walrus_type_map( + self, expr: Expression, type_map: dict[Expression, Type] + ) -> None: + """Collect type narrowings from walrus assignments nested anywhere in expr. + + Unlike _propagate_walrus_assignments, this recurses into arbitrary + expression types (OpExpr, CallExpr, UnaryExpr, etc.) to find any + AssignmentExpr nodes and register the assigned type for narrowing. + This is used when processing the true-branch of an `and` expression, + where any walrus in the right operand is guaranteed to have executed. + """ + if isinstance(expr, AssignmentExpr): + assigned_type = self.lookup_type_or_none(expr.value) + target = collapse_walrus(expr) + if assigned_type is not None: + type_map[target] = assigned_type + self._collect_walrus_type_map(expr.value, type_map) + elif isinstance(expr, OpExpr): + self._collect_walrus_type_map(expr.left, type_map) + self._collect_walrus_type_map(expr.right, type_map) + elif isinstance(expr, UnaryExpr): + self._collect_walrus_type_map(expr.expr, type_map) + elif isinstance(expr, CallExpr): + for arg in expr.args: + self._collect_walrus_type_map(arg, type_map) + elif isinstance(expr, MemberExpr): + self._collect_walrus_type_map(expr.expr, type_map) + elif isinstance(expr, IndexExpr): + self._collect_walrus_type_map(expr.base, type_map) + self._collect_walrus_type_map(expr.index, type_map) + elif isinstance(expr, (TupleExpr, ListExpr)): + for item in expr.items: + self._collect_walrus_type_map(item, type_map) + def is_len_of_tuple(self, expr: Expression) -> bool: """Is this expression a `len(x)` call where x is a tuple or union of tuples?""" if not isinstance(expr, CallExpr): From 82d2147ea0af9681815c33fa2c77a07d2375cc04 Mon Sep 17 00:00:00 2001 From: traveltamers Date: Thu, 2 Apr 2026 12:22:33 -0400 Subject: [PATCH 2/4] fix(typo): restore correct `import` keyword in contextlib import --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index a50312c582e0..6c59972b2ca3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5,7 +5,7 @@ import itertools from collections import defaultdict from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence, Set as AbstractSet -from contextlib importh ExitStack, contextmanager +from contextlib import ExitStack, contextmanager from typing import ( Final, Generic, From 0b098ac255a0d05eb555a260045858188fa76bc4 Mon Sep 17 00:00:00 2001 From: traveltamers Date: Thu, 2 Apr 2026 12:27:00 -0400 Subject: [PATCH 3/4] test(narrowing): add regression test for walrus narrowing in `and` operand Add regression tests for walrus operator behavior in nested expressions. --- test-data/unit/check-inference.test | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 3926627df5ad..c0ab968f3cbb 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -4264,6 +4264,28 @@ def check_or_nested(maybe: bool) -> None: reveal_type(bar) # N: Revealed type is "builtins.list[builtins.int]" reveal_type(baz) # N: Revealed type is "builtins.list[builtins.int]" +[case testInferWalrusAssignmentNestedInAndExpr] +# Walrus narrowing should propagate when the assignment is nested inside +# an arbitrary expression on the right side of an "and" condition. +# Regression test for https://github.com/python/mypy/issues/19430 +class Node: + def __init__(self, value: bool) -> None: + self.value = value + +def check_walrus_in_unary(cond: bool) -> None: + woo = None + if cond and not (woo := Node(True)).value: + reveal_type(woo) # N: Revealed type is "__main__.Node" + else: + reveal_type(woo) # N: Revealed type is "__main__.Node | None" + +def check_walrus_in_nested_call(cond: bool) -> None: + woo = None + if cond and (woo := Node(True)).value: + reveal_type(woo) # N: Revealed type is "__main__.Node" + else: + reveal_type(woo) # N: Revealed type is "__main__.Node | None" + [case testInferOptionalAgainstAny] from typing import Any, Optional, TypeVar From 204a671850f36a358cab9257e7741b4a83ebdcf8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:30:02 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6c59972b2ca3..20f4e0fb141b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7127,9 +7127,7 @@ def _propagate_walrus_assignments( return parent_expr return expr - def _collect_walrus_type_map( - self, expr: Expression, type_map: dict[Expression, Type] - ) -> None: + def _collect_walrus_type_map(self, expr: Expression, type_map: dict[Expression, Type]) -> None: """Collect type narrowings from walrus assignments nested anywhere in expr. Unlike _propagate_walrus_assignments, this recurses into arbitrary