Skip to content

Commit 8bbdd30

Browse files
committed
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
1 parent 1462b4e commit 8bbdd30

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

mypy/checkexpr.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6040,7 +6040,15 @@ def check_for_comp(self, e: GeneratorExpr | DictionaryComprehension) -> None:
60406040

60416041
# values are only part of the comprehension when all conditions are true
60426042
true_map, false_map = self.chk.find_isinstance_check(condition)
6043-
self.chk.push_type_map(true_map)
6043+
if mypy.checker.is_unreachable_map(true_map):
6044+
# The condition can never be true. Unlike statements, the
6045+
# left expression is still type checked, so apply the
6046+
# impossible narrowing to the binder instead of marking
6047+
# the frame unreachable, which would discard it (#21635).
6048+
for expr, typ in true_map.items():
6049+
self.chk.binder.put(expr, typ)
6050+
else:
6051+
self.chk.push_type_map(true_map)
60446052

60456053
if codes.REDUNDANT_EXPR in self.chk.options.enabled_error_codes:
60466054
if mypy.checker.is_unreachable_map(true_map):

test-data/unit/check-isinstance.test

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,37 @@ reveal_type(g) # N: Revealed type is "typing.Generator[builtins.int, None, None]
16141614
reveal_type(d) # N: Revealed type is "builtins.dict[builtins.int, builtins.int]"
16151615
[builtins fixtures/isinstancelist.pyi]
16161616

1617+
[case testComprehensionIsInstanceImpossibleIntersection]
1618+
# https://github.com/python/mypy/issues/21635
1619+
from typing import List
1620+
1621+
class X:
1622+
def f(self) -> int:
1623+
return 0
1624+
class Y:
1625+
def f(self) -> str:
1626+
return ''
1627+
1628+
xs: List[X] = []
1629+
l: List[Y] = [x for x in xs if isinstance(x, Y)]
1630+
g = (reveal_type(x) for x in xs if isinstance(x, Y)) # N: Revealed type is "Never"
1631+
d = {0: x for x in xs if isinstance(x, Y)}
1632+
reveal_type(d) # N: Revealed type is "builtins.dict[builtins.int, Never]"
1633+
[builtins fixtures/isinstancelist.pyi]
1634+
1635+
[case testComprehensionIsSubclassImpossibleIntersectionFinal]
1636+
# https://github.com/python/mypy/issues/21635
1637+
from typing import List, Type
1638+
from typing_extensions import final
1639+
1640+
@final
1641+
class F: ...
1642+
class Other: ...
1643+
1644+
os: List[Other] = []
1645+
fs: List[F] = [o for o in os if isinstance(o, F)]
1646+
[builtins fixtures/isinstancelist.pyi]
1647+
16171648
[case testIsinstanceInWrongOrderInBooleanOp]
16181649
# flags: --warn-unreachable
16191650
class A:

0 commit comments

Comments
 (0)