From f16e5f9837122f69080acb5dd81260a7e43a8ce0 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 3 Apr 2026 17:46:51 -0700 Subject: [PATCH] Narrow Any in and_conditional_map --- mypy/checker.py | 18 +++++++++++------- test-data/unit/check-isinstance.test | 20 ++++++++++++++++++++ test-data/unit/check-narrowing.test | 4 ++-- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8775f1ddef29..0ff349f85a03 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8625,15 +8625,19 @@ def and_conditional_maps(m1: TypeMap, m2: TypeMap, *, use_meet: bool = False) -> the truth of e2. """ # Both conditions can be true; combine the information. Anything - # we learn from either conditions' truth is valid. If the same - # expression's type is refined by both conditions, we somewhat - # arbitrarily give precedence to m2 unless m1 value is Any. - # In the future, we could use an intersection type or meet_types(). + # we learn from either conditions' truth is valid. + # If the same expression's type is refined by both conditions and use_meet=False, we somewhat + # arbitrarily give precedence to m2 unless m2's value is Any or m1's value is Never. result = m2.copy() - m2_keys = {literal_hash(n2) for n2 in m2} + m2_exprs = {literal_hash(n2): n2 for n2 in m2} for n1 in m1: - if literal_hash(n1) not in m2_keys or isinstance( - get_proper_type(m1[n1]), (AnyType, UninhabitedType) + n1_hash = literal_hash(n1) + if n1_hash not in m2_exprs or ( + not use_meet + and ( + isinstance(get_proper_type(m1[n1]), UninhabitedType) + or isinstance(get_proper_type(m2[m2_exprs[n1_hash]]), AnyType) + ) ): result[n1] = m1[n1] if use_meet: diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 310145dfc4ef..3fbea4aa1e81 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1478,6 +1478,26 @@ def f(x: Any): x + "foo" # E: Unsupported operand types for + ("int" and "str") [builtins fixtures/isinstance.pyi] +[case testIsinstanceNarrowingAny] +# flags: --warn-unreachable +from typing import Any + +class A: pass +class B(A): pass + +def f1(x: Any) -> None: + if x and isinstance(x, A): + reveal_type(x) # N: Revealed type is "__main__.A" + if isinstance(x, A) and x: + reveal_type(x) # N: Revealed type is "__main__.A" + +def f2(x: Any) -> None: + if isinstance(x, A) and not isinstance(x, B): + reveal_type(x) # N: Revealed type is "__main__.A" + if not isinstance(x, B) and isinstance(x, A): + reveal_type(x) # N: Revealed type is "__main__.A" +[builtins fixtures/isinstance.pyi] + [case testIsinstanceOfGenericClassRetainsParameters] # flags: --warn-unreachable from typing import List, Union diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 982a86e38edd..7410cc032654 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -1727,7 +1727,7 @@ reveal_type(c) # N: Revealed type is "__main__.C" c1: C if isinstance(c1, tp) and isinstance(c1, D): - reveal_type(c1) # N: Revealed type is "Any" + reveal_type(c1) # N: Revealed type is "__main__.D" else: reveal_type(c1) # N: Revealed type is "__main__.C" reveal_type(c1) # N: Revealed type is "__main__.C" @@ -1741,7 +1741,7 @@ reveal_type(c2) # N: Revealed type is "__main__.C" c3: C if isinstance(c3, D) and isinstance(c3, tp): - reveal_type(c3) # N: Revealed type is "Any" + reveal_type(c3) # N: Revealed type is "__main__.D" else: reveal_type(c3) # N: Revealed type is "__main__.C" reveal_type(c3) # N: Revealed type is "__main__.C"