From 8bbdd305557a41a599842c1d5724aab3fdd2faf8 Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Thu, 2 Jul 2026 18:38:37 +0530 Subject: [PATCH] Fix narrowing in comprehensions when the intersection is impossible When a comprehension condition narrows the index variable to an impossible type (e.g. isinstance/issubclass where mypy determines a subclass of both classes cannot exist, or the class is @final), check_for_comp() pushed an unreachable type map, which just marked the binder frame unreachable without recording any narrowing. Unlike statements, the comprehension's left expression is still type checked, so it was checked with the *unnarrowed* type, producing false positives like: error: List comprehension has incompatible type List[type[A]]; expected List[type[M]] Apply the impossible narrowing to the binder instead, so the left expression checks against Never, matching the narrowing an equivalent if statement would produce. Fixes #21635 --- mypy/checkexpr.py | 10 ++++++++- test-data/unit/check-isinstance.test | 31 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 44855f49afaf9..04035c07c4c13 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6040,7 +6040,15 @@ def check_for_comp(self, e: GeneratorExpr | DictionaryComprehension) -> None: # values are only part of the comprehension when all conditions are true true_map, false_map = self.chk.find_isinstance_check(condition) - self.chk.push_type_map(true_map) + if mypy.checker.is_unreachable_map(true_map): + # The condition can never be true. Unlike statements, the + # left expression is still type checked, so apply the + # impossible narrowing to the binder instead of marking + # the frame unreachable, which would discard it (#21635). + for expr, typ in true_map.items(): + self.chk.binder.put(expr, typ) + else: + self.chk.push_type_map(true_map) if codes.REDUNDANT_EXPR in self.chk.options.enabled_error_codes: if mypy.checker.is_unreachable_map(true_map): diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index acd81839fcdc7..c0ae4db3eddd0 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1614,6 +1614,37 @@ reveal_type(g) # N: Revealed type is "typing.Generator[builtins.int, None, None] reveal_type(d) # N: Revealed type is "builtins.dict[builtins.int, builtins.int]" [builtins fixtures/isinstancelist.pyi] +[case testComprehensionIsInstanceImpossibleIntersection] +# https://github.com/python/mypy/issues/21635 +from typing import List + +class X: + def f(self) -> int: + return 0 +class Y: + def f(self) -> str: + return '' + +xs: List[X] = [] +l: List[Y] = [x for x in xs if isinstance(x, Y)] +g = (reveal_type(x) for x in xs if isinstance(x, Y)) # N: Revealed type is "Never" +d = {0: x for x in xs if isinstance(x, Y)} +reveal_type(d) # N: Revealed type is "builtins.dict[builtins.int, Never]" +[builtins fixtures/isinstancelist.pyi] + +[case testComprehensionIsSubclassImpossibleIntersectionFinal] +# https://github.com/python/mypy/issues/21635 +from typing import List, Type +from typing_extensions import final + +@final +class F: ... +class Other: ... + +os: List[Other] = [] +fs: List[F] = [o for o in os if isinstance(o, F)] +[builtins fixtures/isinstancelist.pyi] + [case testIsinstanceInWrongOrderInBooleanOp] # flags: --warn-unreachable class A: