From dbea92a52ce97b934917fde4233ff2430646ecef Mon Sep 17 00:00:00 2001 From: JiwaniZakir Date: Tue, 19 May 2026 20:14:33 +0000 Subject: [PATCH 1/2] Fix value-in-container narrowing widening to non-subtypes (#21512) When narrowing `x` in `x in container`, mypy used __eq__-based narrowing which could widen `x` to the container's item type even when that type is not a proper subtype of `x`'s declared type. Discard the if-branch narrowing result when it is not a proper subtype of the original type. --- mypy/checker.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 2cb3d69b2d77..f51a1e81572d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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` From 6420e2749f59aa0e693c699dc1da6f3d60a879a7 Mon Sep 17 00:00:00 2001 From: JiwaniZakir Date: Tue, 19 May 2026 20:19:20 +0000 Subject: [PATCH 2/2] Add regression test for value-in-container narrowing widening (#21512) Adds a test case to check-narrowing.test to cover the regression where `x in container` narrowing would widen x to the container's item type even when that item type is not a proper subtype of x's declared type. Co-Authored-By: Claude Sonnet 4.6 --- test-data/unit/check-narrowing.test | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 7bab7baa6ceb..ec068d5210dc 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -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]