Repository: python/mypy

Issue:
Defining _order_ in an enum class rejected
This generates an error, even though it's valid:

```py
from enum import Enum

class E(Enum):
    _order_ = 'X Y'  # Cannot override writable attribute "_order_" with a final one
    X = 1
    Y = 2
```

According to the [docs](https://docs.python.org/3/library/enum.html#supported-sunder-names), `_order_` is a somewhat magical attribute, but it's okay to define it.

The error started happening as a side effect of #10852. 

It looks like `_order_` actually isn't a valid attribute at runtime, so a potential fix would be to remove it from typeshed. If we want to do this right, we should also remove an explicit definition from the TypeInfo, since it's not available at runtime.

@sobolevn What do you think?

This is a regression, so marking as high priority.


Discussion:
I think that `Enum` subclasses should be special cased for `Cannot override writable attribute "_order_" with a final one` rule. Because, a lot of things like 

```python
from enum import Enum

class E(Enum):
    value = 1  # E: Cannot override writable attribute "value" with a final one

print(E.value)
```

work just fine in runtime, but `mypy` is not really happy.

Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -50,6 +50,7 @@
     type_object_type,
     analyze_decorator_or_funcbase_access,
 )
+from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS
 from mypy.typeops import (
     map_type_from_supertype, bind_self, erase_to_bound, make_simplified_union,
     erase_def_to_union_or_bound, erase_to_union_or_bound, coerce_to_literal,
@@ -2522,13 +2523,14 @@ def check_compatibility_final_super(self, node: Var,
             self.msg.cant_override_final(node.name, base.name, node)
             return False
         if node.is_final:
+            if base.fullname in ENUM_BASES and node.name in ENUM_SPECIAL_PROPS:
+                return True
             self.check_if_final_var_override_writable(node.name, base_node, node)
         return True
 
     def check_if_final_var_override_writable(self,
                                              name: str,
-                                             base_node:
-                                             Optional[Node],
+                                             base_node: Optional[Node],
                                              ctx: Context) -> None:
         """Check that a final variable doesn't override writeable attribute.
 
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -14,6 +14,7 @@
     has_any_from_unimported_type, check_for_explicit_any, set_any_tvars, expand_type_alias,
     make_optional_type,
 )
+from mypy.semanal_enum import ENUM_BASES
 from mypy.types import (
     Type, AnyType, CallableType, Overloaded, NoneType, TypeVarType,
     TupleType, TypedDictType, Instance, ErasedType, UnionType,
@@ -1000,9 +1001,7 @@ def check_callable_call(self,
         ret_type = get_proper_type(callee.ret_type)
         if callee.is_type_obj() and isinstance(ret_type, Instance):
             callable_name = ret_type.type.fullname
-        if (isinstance(callable_node, RefExpr)
-            and callable_node.fullname in ('enum.Enum', 'enum.IntEnum',
-                                           'enum.Flag', 'enum.IntFlag')):
+        if isinstance(callable_node, RefExpr) and callable_node.fullname in ENUM_BASES:
             # An Enum() call that failed SemanticAnalyzerPass2.check_enum_call().
             return callee.ret_type, callee
 
diff --git a/mypy/plugins/enums.py b/mypy/plugins/enums.py
--- a/mypy/plugins/enums.py
+++ b/mypy/plugins/enums.py
@@ -18,15 +18,13 @@
 from mypy.typeops import make_simplified_union
 from mypy.nodes import TypeInfo
 from mypy.subtypes import is_equivalent
+from mypy.semanal_enum import ENUM_BASES
 
-# Note: 'enum.EnumMeta' is deliberately excluded from this list. Classes that directly use
-# enum.EnumMeta do not necessarily automatically have the 'name' and 'value' attributes.
-ENUM_PREFIXES: Final = {"enum.Enum", "enum.IntEnum", "enum.Flag", "enum.IntFlag"}
-ENUM_NAME_ACCESS: Final = {"{}.name".format(prefix) for prefix in ENUM_PREFIXES} | {
-    "{}._name_".format(prefix) for prefix in ENUM_PREFIXES
+ENUM_NAME_ACCESS: Final = {"{}.name".format(prefix) for prefix in ENUM_BASES} | {
+    "{}._name_".format(prefix) for prefix in ENUM_BASES
 }
-ENUM_VALUE_ACCESS: Final = {"{}.value".format(prefix) for prefix in ENUM_PREFIXES} | {
-    "{}._value_".format(prefix) for prefix in ENUM_PREFIXES
+ENUM_VALUE_ACCESS: Final = {"{}.value".format(prefix) for prefix in ENUM_BASES} | {
+    "{}._value_".format(prefix) for prefix in ENUM_BASES
 }
 
 
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -116,7 +116,7 @@
 )
 from mypy.semanal_namedtuple import NamedTupleAnalyzer
 from mypy.semanal_typeddict import TypedDictAnalyzer
-from mypy.semanal_enum import EnumCallAnalyzer
+from mypy.semanal_enum import EnumCallAnalyzer, ENUM_BASES
 from mypy.semanal_newtype import NewTypeAnalyzer
 from mypy.reachability import (
     infer_reachability_of_if_statement, infer_condition_value, ALWAYS_FALSE, ALWAYS_TRUE,
@@ -1554,8 +1554,7 @@ def configure_base_classes(self,
                     self.fail('Cannot subclass "NewType"', defn)
                 if (
                     base.type.is_enum
-                    and base.type.fullname not in (
-                        'enum.Enum', 'enum.IntEnum', 'enum.Flag', 'enum.IntFlag')
+                    and base.type.fullname not in ENUM_BASES
                     and base.type.names
                     and any(not isinstance(n.node, (FuncBase, Decorator))
                             for n in base.type.names.values())
diff --git a/mypy/semanal_enum.py b/mypy/semanal_enum.py
--- a/mypy/semanal_enum.py
+++ b/mypy/semanal_enum.py
@@ -4,6 +4,7 @@
 """
 
 from typing import List, Tuple, Optional, Union, cast
+from typing_extensions import Final
 
 from mypy.nodes import (
     Expression, Context, TypeInfo, AssignmentStmt, NameExpr, CallExpr, RefExpr, StrExpr,
@@ -13,6 +14,15 @@
 from mypy.semanal_shared import SemanticAnalyzerInterface
 from mypy.options import Options
 
+# Note: 'enum.EnumMeta' is deliberately excluded from this list. Classes that directly use
+# enum.EnumMeta do not necessarily automatically have the 'name' and 'value' attributes.
+ENUM_BASES: Final = frozenset((
+    'enum.Enum', 'enum.IntEnum', 'enum.Flag', 'enum.IntFlag',
+))
+ENUM_SPECIAL_PROPS: Final = frozenset((
+    'name', 'value', '_name_', '_value_', '_order_', '__order__',
+))
+
 
 class EnumCallAnalyzer:
     def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
@@ -62,7 +72,7 @@ class A(enum.Enum):
         if not isinstance(callee, RefExpr):
             return None
         fullname = callee.fullname
-        if fullname not in ('enum.Enum', 'enum.IntEnum', 'enum.Flag', 'enum.IntFlag'):
+        if fullname not in ENUM_BASES:
             return None
         items, values, ok = self.parse_enum_call_args(call, fullname.split('.')[-1])
         if not ok:


Test patch:
diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test
--- a/test-data/unit/check-enum.test
+++ b/test-data/unit/check-enum.test
@@ -1392,7 +1392,7 @@ Medal.silver = 2  # E: Cannot assign to final attribute "silver"
 from enum import Enum
 class Types(Enum):
     key = 0
-    value = 1  # E: Cannot override writable attribute "value" with a final one
+    value = 1
 
 
 [case testEnumReusedKeys]
@@ -1677,3 +1677,28 @@ class A(Enum):
     class Inner: pass
 class B(A): pass  # E: Cannot inherit from final class "A"
 [builtins fixtures/bool.pyi]
+
+
+[case testEnumFinalSpecialProps]
+# https://github.com/python/mypy/issues/11699
+from enum import Enum, IntEnum
+
+class E(Enum):
+    name = 'a'
+    value = 'b'
+    _name_ = 'a1'
+    _value_ = 'b2'
+    _order_ = 'X Y'
+    __order__ = 'X Y'
+
+class EI(IntEnum):
+    name = 'a'
+    value = 1
+    _name_ = 'a1'
+    _value_ = 2
+    _order_ = 'X Y'
+    __order__ = 'X Y'
+
+E._order_ = 'a'  # E: Cannot assign to final attribute "_order_"
+EI.value = 2     # E: Cannot assign to final attribute "value"
+[builtins fixtures/bool.pyi]