Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6925,7 +6925,16 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa
narrowable_indices={0},
)
all_if_maps.append(if_map)
if is_singleton_equality_type(get_proper_type(known_item)):
# A literal expression in the container, e.g. `x in ('a', 'b')`,
# gives the item an instance type with a last known value rather
# than a literal type, but it still denotes a single value, so a
# failed comparison against it is enough for negative narrowing.
# Only this check coerces; the type we narrowed against above is
# left alone, to keep `in` consistent with `==`.
is_single_valued = is_singleton_equality_type(
get_proper_type(coerce_to_literal(known_item))
)
if is_single_valued and not has_custom_eq_checks(p_known_item):
all_else_maps.append(else_map)
if_map = reduce_or_conditional_type_maps(all_if_maps)
else_map = reduce_and_conditional_type_maps(all_else_maps, use_meet=True)
Expand Down
58 changes: 52 additions & 6 deletions test-data/unit/check-narrowing.test
Original file line number Diff line number Diff line change
Expand Up @@ -3251,14 +3251,13 @@ def narrow_tuple_exact(x: Literal['a', 'b', 'c'], t: tuple[Literal['a'], Literal
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"

def narrow_tuple_expression(x: Literal['a', 'b', 'c']):
# TODO: this should match narrow_tuple_exact
if x in ('a', 'b'):
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"
else:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']"
reveal_type(x) # N: Revealed type is "Literal['c']"

if x not in ('a', 'b'):
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']"
reveal_type(x) # N: Revealed type is "Literal['c']"
else:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"

Expand Down Expand Up @@ -3294,7 +3293,7 @@ def narrow_list(x: Literal['a', 'b', 'c'], t: list[Literal['a', 'b']]):
if x in ['a', 'b']:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"
else:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']"
reveal_type(x) # N: Revealed type is "Literal['c']"

if x in ['a', 'b', *[]]:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']"
Expand All @@ -3310,7 +3309,7 @@ def narrow_set(x: Literal['a', 'b', 'c'], t: set[Literal['a', 'b']]):
if x in {'a', 'b'}:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"
else:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']"
reveal_type(x) # N: Revealed type is "Literal['c']"

if x in {'a', 'b', *[]}:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']"
Expand All @@ -3326,7 +3325,7 @@ def narrow_dict(x: Literal['a', 'b', 'c'], t: dict[Literal['a', 'b'], int]):
if x in {'a': 0, 'b': 1}:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"
else:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']"
reveal_type(x) # N: Revealed type is "Literal['c']"

if x in {'a': 0, 'b': 1, **{}}:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b'] | Literal['c']"
Expand All @@ -3335,6 +3334,53 @@ def narrow_dict(x: Literal['a', 'b', 'c'], t: dict[Literal['a', 'b'], int]):
[builtins fixtures/primitives.pyi]


[case testNarrowNotInLiteralContainer]
# flags: --strict-equality --warn-unreachable
from typing import Final, Literal

VALID: Final = ('a', 'b')

def narrow_int_literals(x: Literal[1, 2, 3]):
if x not in (1, 2):
reveal_type(x) # N: Revealed type is "Literal[3]"
else:
reveal_type(x) # N: Revealed type is "Literal[1] | Literal[2]"

def narrow_final_tuple(x: Literal['a', 'b', 'c']):
if x not in VALID:
reveal_type(x) # N: Revealed type is "Literal['c']"
else:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"

def narrow_mixed_container(x: Literal['a', 'b'] | None):
if x not in ('a', None):
reveal_type(x) # N: Revealed type is "Literal['b']"
else:
reveal_type(x) # N: Revealed type is "Literal['a'] | None"

def exhaustive_check(x: Literal['a', 'b']) -> int:
if x in ('a',):
return 1
elif x in ('b',):
return 2
return 0 # E: Statement is unreachable

def non_literal_item(x: Literal['a', 'b'], y: str):
# `y` denotes more than one value, so the negative branch stays wide
if x not in (y,):
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"
else:
reveal_type(x) # N: Revealed type is "Literal['a'] | Literal['b']"

def bool_int_overlap(x: Literal[0, 1, 2]):
# `False == 0` and `True == 1`, so this must stay as wide as `x != False`
if x not in (False,):
reveal_type(x) # N: Revealed type is "Literal[0] | Literal[1] | Literal[2]"
else:
reveal_type(x) # N: Revealed type is "Literal[0] | Literal[1] | Literal[2]"
[builtins fixtures/primitives.pyi]


[case testNarrowCustomEqEnumInLiteralContainer]
# flags: --strict-equality --warn-unreachable
# https://github.com/python/mypy/issues/21703
Expand Down
Loading