Skip to content

Support attribute access on enum members correctly #19422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,18 @@ def analyze_var(
result = AnyType(TypeOfAny.special_form)
fullname = f"{var.info.fullname}.{name}"
hook = mx.chk.plugin.get_attribute_hook(fullname)

if var.info.is_enum and not mx.is_lvalue:
if name in var.info.enum_members and name not in {"name", "value"}:
enum_literal = LiteralType(name, fallback=itype)
result = itype.copy_modified(last_known_value=enum_literal)
elif (
isinstance(p_result := get_proper_type(result), Instance)
and p_result.type.fullname == "enum.nonmember"
and p_result.args
):
# Unwrap nonmember similar to class-level access
result = p_result.args[0]
if result and not (implicit or var.info.is_protocol and is_instance_var(var)):
result = analyze_descriptor_access(result, mx)
if hook:
Expand Down
2 changes: 1 addition & 1 deletion mypy/plugins/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def _implements_new(info: TypeInfo) -> bool:
def enum_member_callback(ctx: mypy.plugin.FunctionContext) -> Type:
"""By default `member(1)` will be inferred as `member[int]`,
we want to improve the inference to be `Literal[1]` here."""
if ctx.arg_types or ctx.arg_types[0]:
if ctx.arg_types and ctx.arg_types[0]:
arg = get_proper_type(ctx.arg_types[0][0])
proper_return = get_proper_type(ctx.default_return_type)
if (
Expand Down
56 changes: 51 additions & 5 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -1953,7 +1953,8 @@ class A(Enum):
x: int
def method(self) -> int: pass
class B(A):
x = 1 # E: Cannot override writable attribute "x" with a final one
x = 1 # E: Cannot override writable attribute "x" with a final one \
# E: Incompatible types in assignment (expression has type "B", base class "A" defined the type as "int")

class A1(Enum):
x: int = 1 # E: Enum members must be left unannotated \
Expand All @@ -1971,8 +1972,8 @@ class B2(A2): # E: Cannot extend enum with existing members: "A2"
class A3(Enum):
x: Final[int] # type: ignore
class B3(A3):
x = 1 # E: Cannot override final attribute "x" (previously declared in base class "A3")

x = 1 # E: Cannot override final attribute "x" (previously declared in base class "A3") \
Copy link
Collaborator Author

@sterliakov sterliakov Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this (and other equivalent) change is correct: as per the spec, unannotated attrs should be interpreted as nonmembers, so, strictly speaking, you're assigning a soon-to-become enum instance to something of type int, breaking the interface. And it doesn't matter much as there is already another diagnostic on this line.

# E: Incompatible types in assignment (expression has type "B3", base class "A3" defined the type as "int")
[builtins fixtures/bool.pyi]

[case testEnumNotFinalWithMethodsAndUninitializedValuesStub]
Expand All @@ -1984,14 +1985,16 @@ class A(Enum): # E: Detected enum "lib.A" in a type stub with zero members. The
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members
x: int
class B(A):
x = 1 # E: Cannot override writable attribute "x" with a final one
x = 1 # E: Cannot override writable attribute "x" with a final one \
# E: Incompatible types in assignment (expression has type "B", base class "A" defined the type as "int")

class C(Enum):
x = 1
class D(C): # E: Cannot extend enum with existing members: "C" \
# E: Detected enum "lib.D" in a type stub with zero members. There is a chance this is due to a recent change in the semantics of enum membership. If so, use `member = value` to mark an enum member, instead of `member: type` \
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members
x: int # E: Cannot assign to final name "x"
x: int # E: Incompatible types in assignment (expression has type "int", base class "C" defined the type as "C") \
# E: Cannot assign to final name "x"
[builtins fixtures/bool.pyi]

[case testEnumNotFinalWithMethodsAndUninitializedValuesStubMember]
Expand Down Expand Up @@ -2419,6 +2422,49 @@ def some_a(a: A):
reveal_type(a) # N: Revealed type is "Literal[__main__.A.x]"
[builtins fixtures/dict.pyi]

[case testEnumAccessFromInstance]
# flags: --python-version 3.11 --warn-unreachable
# This was added in 3.11
from enum import Enum, member, nonmember

class A(Enum):
x = 1
y = member(2)
z = nonmember(3)

reveal_type(A.x) # N: Revealed type is "Literal[__main__.A.x]?"
reveal_type(A.y) # N: Revealed type is "Literal[__main__.A.y]?"
reveal_type(A.z) # N: Revealed type is "builtins.int"

reveal_type(A.x.x) # N: Revealed type is "Literal[__main__.A.x]?"
reveal_type(A.x.x.x) # N: Revealed type is "Literal[__main__.A.x]?"
reveal_type(A.x.y) # N: Revealed type is "Literal[__main__.A.y]?"
reveal_type(A.x.y.y) # N: Revealed type is "Literal[__main__.A.y]?"
reveal_type(A.x.z) # N: Revealed type is "builtins.int"

reveal_type(A.y.x) # N: Revealed type is "Literal[__main__.A.x]?"
reveal_type(A.y.y) # N: Revealed type is "Literal[__main__.A.y]?"
reveal_type(A.y.z) # N: Revealed type is "builtins.int"

A.z.x # E: "int" has no attribute "x"

class B(Enum):
x = 1
value = 2

reveal_type(B.x) # N: Revealed type is "Literal[__main__.B.x]?"
reveal_type(B.x.value) # N: Revealed type is "Literal[2]?"
reveal_type(B.x.x.value) # N: Revealed type is "Literal[2]?"
B.x.value.value # E: "int" has no attribute "value"
B.x.value.value.value # E: "int" has no attribute "value"
reveal_type(B.value) # N: Revealed type is "Literal[__main__.B.value]?"
reveal_type(B.value.x) # N: Revealed type is "Literal[__main__.B.x]?"
reveal_type(B.value.x.x) # N: Revealed type is "Literal[__main__.B.x]?"
reveal_type(B.value.x.value) # N: Revealed type is "Literal[2]?"
B.value.x.value.value # E: "int" has no attribute "value"
B.value.value.value # E: "int" has no attribute "value"
[builtins fixtures/dict.pyi]


[case testErrorOnAnnotatedMember]
from enum import Enum
Expand Down
2 changes: 2 additions & 0 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -5675,10 +5675,12 @@ class FinalEnum(Enum):
[builtins fixtures/isinstance.pyi]
[out]
main:3: error: Cannot override writable attribute "x" with a final one
main:3: error: Incompatible types in assignment (expression has type "Ok", base class "RegularEnum" defined the type as "int")
main:4: error: Cannot extend enum with existing members: "FinalEnum"
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")
[out2]
main:3: error: Cannot override writable attribute "x" with a final one
main:3: error: Incompatible types in assignment (expression has type "Ok", base class "RegularEnum" defined the type as "int")
main:4: error: Cannot extend enum with existing members: "FinalEnum"
main:5: error: Cannot override final attribute "x" (previously declared in base class "FinalEnum")

Expand Down