pythonpython-attrs

Enum of dataclass works but frozen attrs doesn't


The built-in enum provides a way to create enums of primitive types (IntEnum, StrEnum).

I'd like to create an enum of structured objects.

One way to do that is with dataclass, and it works:

from dataclasses import dataclass
from enum import Enum

from typing_extensions import assert_never

@dataclass(frozen=True)
class _BaseInstTypeDataclass:
    instance_type: str
    display_name: str


class InstTypeDataClass(_BaseInstTypeDataclass, Enum):
    t2_micro = ("t2.micro", "t2.micro: Cheap!")
    r7i_2xlarge = ("r7i.2xlarge", "r7i.2xlarge: Expensive!")


assert list(InstTypeDataClass) == [
    InstTypeDataClass.t2_micro,
    InstTypeDataClass.r7i_2xlarge,
]
assert isinstance(InstTypeDataClass.t2_micro, InstTypeDataClass)


# This function type checks
def f_dataclass(e: InstTypeDataClass):
    if e == InstTypeDataClass.t2_micro:
        ...
    elif e == InstTypeDataClass.r7i_2xlarge:
        ...
    else:
        assert_never(e)

Both the static- (via pyright) and runtime-behavior is as expected.

However, with attrs...:

import attrs


@attrs.define(frozen=True)
class _BaseInstTypeAttrs:
    instance_type: str
    display_name: str


class InstTypeAttrs(_BaseInstTypeAttrs, Enum):
    t2_micro = ("t2.micro", "t2.micro: Cheap!")
    r7i_2xlarge = ("r7i.2xlarge", "r7i.2xlarge: Expensive!")


# This function type checks
def f_attrs(e: InstTypeAttrs):
    if e == InstTypeAttrs.t2_micro:
        ...
    elif e == InstTypeAttrs.r7i_2xlarge:
        ...
    else:
        assert_never(e)

... the type checker is happy, but at run-time...:

$ python foo.py 
_BaseInstTypeDataclass(instance_type='foo', display_name='bar')
Traceback (most recent call last):
  File "foo.py", line 58, in <module>
    class InstTypeAttrs(_BaseInstTypeAttrs, Enum):
  File "/Users/.../python3.10/enum.py", line 287, in __new__
    enum_member._value_ = value
  File "/Users/.../python3.10/site-packages/attr/_make.py", line 551, in _frozen_setattrs
    raise FrozenInstanceError()
attr.exceptions.FrozenInstanceError

Is this because... Enum and attrs.define(frozen=True) don't play nice...?


Solution

  • Frozen dataclasses only block assignment to fields. Frozen attrs classes block all attribute assignment (except for a weird special case where the instance is an exception object).

    That means that when the enum internals try to set the enum member's _value_ attribute, the frozen dataclass accepts the assignment, because _value_ isn't a dataclass field. The attrs-based class rejects the assignment.