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
10 changes: 10 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6831,6 +6831,16 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa
# We can't do negative narrowing, since e.g. the container could
# just be empty.
else_map = {}
# The `in` operator uses __eq__, not isinstance. Knowing that
# value == some_container_element does not imply value has the
# runtime type of that element. Discard any narrowing that would
# widen item_type to a non-subtype (e.g. tuple[int,int,int] must
# not be widened to a subclass Size just because they compare equal).
left_expr = collapse_walrus(operands[left_index])
if left_expr in if_map and not is_proper_subtype(
if_map[left_expr], item_type, ignore_promotions=True
):
del if_map[left_expr]

if right_index in narrowable_operand_index_to_hash:
# E.g. narrows the right operand in `if "key" in typed_dict`
Expand Down
14 changes: 14 additions & 0 deletions test-data/unit/check-narrowing.test
Original file line number Diff line number Diff line change
Expand Up @@ -4281,3 +4281,17 @@ def func(y: H) -> H:
else:
return y
[builtins fixtures/primitives.pyi]

[case testValueInContainerNoWideningToNonSubtype]
# Regression test for https://github.com/python/mypy/issues/21512
# Narrowing x in `if x in container` must not widen x to the container item
# type when that item type is not a subtype of x's declared type.
class Size(tuple[int, ...]):
def numel(self) -> int: ...

sizes: list[Size]
value = (1, 2, 3)
if value in sizes:
reveal_type(value) # N: Revealed type is "tuple[builtins.int, builtins.int, builtins.int]"
value.numel() # E: "tuple[int, int, int]" has no attribute "numel"
[builtins fixtures/primitives.pyi]
Loading