From 9a655f1e97d87cf38737a570bb1126adb5d3e4ab Mon Sep 17 00:00:00 2001 From: Friday Date: Tue, 17 Feb 2026 13:19:07 +0000 Subject: [PATCH 1/2] Fix enum private names (__x) being incorrectly marked as final Since Python 3.11, private names (names starting with double underscore but not ending with it, like `__config`) are not enum members and should not be treated as final. The semantic analyzer was using `is_dunder()` to skip only dunder names (`__x__`), missing private names (`__x`). This caused false positives like "Cannot assign to final attribute" when reassigning private attributes on an enum. The fix uses `name.startswith("__")` which matches the logic already used by `TypeInfo.enum_members` in nodes.py, ensuring consistency between member detection and finality marking. Fixes #20789 --- mypy/semanal.py | 8 ++++++-- test-data/unit/check-enum.test | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 219459c92e3ce..32e49f83c3088 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3841,9 +3841,13 @@ def store_final_status(self, s: AssignmentStmt) -> None: and isinstance(cur_node.node, Var) and not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs) ): - # Double underscored members are writable on an `Enum`. + # Double underscored names are writable on an `Enum`: + # - Dunders (__x__) are not enum members + # - Private names (__x) are not enum members (since Python 3.11) # (Except read-only `__members__` but that is handled in type checker) - cur_node.node.is_final = s.is_final_def = not is_dunder(cur_node.node.name) + cur_node.node.is_final = s.is_final_def = not cur_node.node.name.startswith( + "__" + ) # Special case: deferred initialization of a final attribute in __init__. # In this case we just pretend this is a valid final definition to suppress diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index c05dfdef2bf7f..15a0b80c33417 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -1602,6 +1602,20 @@ Medal.silver = 2 # E: Cannot assign to final attribute "silver" [builtins fixtures/enum.pyi] +[case testEnumPrivateNameNotFinal] +# Private names (__x) are not enum members and should not be final +from enum import Enum + +class MyEnum(Enum): + __config = "some_value" + A = 1 + B = 2 + +# __config is not a member so reassignment should be allowed +MyEnum._MyEnum__config = "other_value" +[builtins fixtures/enum.pyi] + + [case testEnumFinalValuesCannotRedefineValueProp] from enum import Enum class Types(Enum): From feb466099382add3c94eb1beaaa11934ef93a240 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:21:41 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/semanal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 32e49f83c3088..123e5a0ca0fc5 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -309,7 +309,7 @@ ) from mypy.types_utils import is_invalid_recursive_alias, store_argument_type from mypy.typevars import fill_typevars -from mypy.util import correct_relative_import, is_dunder, module_prefix, unmangle, unnamed_function +from mypy.util import correct_relative_import, module_prefix, unmangle, unnamed_function from mypy.visitor import NodeVisitor T = TypeVar("T") @@ -3845,8 +3845,8 @@ def store_final_status(self, s: AssignmentStmt) -> None: # - Dunders (__x__) are not enum members # - Private names (__x) are not enum members (since Python 3.11) # (Except read-only `__members__` but that is handled in type checker) - cur_node.node.is_final = s.is_final_def = not cur_node.node.name.startswith( - "__" + cur_node.node.is_final = s.is_final_def = ( + not cur_node.node.name.startswith("__") ) # Special case: deferred initialization of a final attribute in __init__.