From 668fe264ce77c16a8173d5a549bf74d5137226a3 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Tue, 19 May 2026 21:09:49 -0700 Subject: [PATCH] Fix false positive comparison-overlap for str/int enum members Mypy tracks enum literals by member name rather than runtime value, so `A.a == "b"` (where `A.a.value == "b"`) was flagged as a non-overlapping equality check under --strict-equality. Mixin enums like `class A(str, Enum)` and IntEnum compare equal to plain values of the mixin type at runtime, so this is a false positive. Treat the comparison as potentially overlapping when one side is an enum literal whose enum class mixes in a base type that the other side is also a value of. Fixes #19576. --- mypy/checkexpr.py | 33 +++++++++++++++++++++++++++++++++ test-data/unit/check-enum.test | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index dd914498df87d..f139106df129d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3990,6 +3990,12 @@ def dangerous_comparison( "builtins.bytearray" ): return False + # Enum members of a mixin class (e.g. `class A(str, Enum)`) may compare + # equal to values of the mixin type at runtime. Mypy tracks enum + # literals by member name rather than runtime value, so we can't tell + # whether the comparison is non-overlapping. Avoid the false positive. + if _enum_mixin_overlaps(left, right) or _enum_mixin_overlaps(right, left): + return False return not is_overlapping_types(left, right, ignore_promotions=False) def check_method_call_by_name( @@ -6936,6 +6942,33 @@ def try_getting_literal(typ: Type) -> ProperType: return typ +def _enum_mixin_overlaps(left: ProperType, right: ProperType) -> bool: + """Return True if `left` is an enum literal whose enum class mixes in a + non-enum base (e.g. `str`, `int`) that `right` is also a value of. + + Enum members of such mixin enums compare equal to plain values of the mixin + type at runtime (e.g. `MyStrEnum.member == "hello"` can be True). Since + mypy represents an enum literal by member name rather than runtime value, + it can't tell whether such a comparison is non-overlapping. Treating it as + potentially overlapping avoids false positives under `--strict-equality`. + """ + if not (isinstance(left, LiteralType) and left.is_enum_literal()): + return False + enum_info = left.fallback.type + # Look for a non-enum, non-object base class that the right side could + # also be an instance of. + for base in enum_info.mro[1:]: + if base.fullname in ("builtins.object",): + continue + if base.is_enum: + continue + if isinstance(right, Instance) and right.type.has_base(base.fullname): + return True + if isinstance(right, LiteralType) and right.fallback.type.has_base(base.fullname): + return True + return False + + def is_expr_literal_type(node: Expression) -> bool: """Returns 'true' if the given node is a Literal""" if isinstance(node, IndexExpr): diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 55a20e2b3fa2c..9cc86a9661cef 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -2798,6 +2798,38 @@ def f1(a: Foo | Literal['foo']) -> Foo: return a [builtins fixtures/primitives.pyi] +[case testStrEnumEqualityMemberNameDiffersFromValue] +# flags: --strict-equality +import enum + +# https://github.com/python/mypy/issues/19576 +# Mypy tracks str-enum literals by member name, not by runtime value, so it +# can't tell whether `A.a == "b"` is non-overlapping. Avoid the false positive. + +class A(str, enum.Enum): + a = "b" + +class B(enum.StrEnum): + a = "b" + +A.a == "a" # OK +A.a == "b" # OK +B.a == "a" # OK +B.a == "b" # OK + +class C(enum.IntEnum): + x = 5 + +C.x == 1 # OK +C.x == 5 # OK +C.x == "x" # E: Non-overlapping equality check (left operand type: "Literal[C.x]", right operand type: "Literal['x']") + +class Plain(enum.Enum): + a = "a" + +Plain.a == "a" # E: Non-overlapping equality check (left operand type: "Literal[Plain.a]", right operand type: "Literal['a']") +[builtins fixtures/primitives.pyi] + [case testStrEnumEqualityAlias] # flags: --strict-equality --warn-unreachable # https://github.com/python/mypy/issues/16830