Search code examples
pythonpython-3.xdictionaryenumshashable

Why are mutable values allowed in Python Enums?


This is somewhat of a follow on to Why are mutable values in Python Enums the same object?.

If the values of an Enum are mutable (e.g. lists, etc.), those values can be changed at any time. I think this poses something of an issue if Enum members are retrieved by value, especially if someone inadvertently changes the value of an Enum he looks up:

>>> from enum import Enum
>>> class Color(Enum):
        black = [1,2]
        blue = [1,2,3]

>>> val_1 = [1,2]
>>> val_2 = [1,2,3]

>>> Color(val_1)
<Color.black: [1, 2]>

>>> Color(val_2)
<Color.blue: [1, 2, 3]>

>>> my_color = Color(val_1)
>>> my_color.value.append(3)

>>> Color(val_2)
<Color.black: [1, 2, 3]>

>>> Color(val_1)
Traceback (most recent call last):
  ...
ValueError: [1, 2] is not a valid Color

I think given normal Python idioms this is okay, with the implication being that users can use mutables as their Enum values, but just to understand the can of worms they might be opening.

However this brings up a second issue - since you can look up an Enum memeber by value, and the value can be mutable, it must be doing the lookup by a means other than a hashmap/dict, since the mutable cannot be a key in such a dict.

Wouldn't it be more efficient (although, granted, less flexible) to limit Enum values to only immutable types so that lookup-by-value could be implemented with a dict?


Solution

  • It appears the answer to my second question was hiding in plain sight in the soure code for enum.py.

    Each Enum does contain a dict of value->member pairs for hashable (i.e. immutable) values, and when you look up an Enum by value, it attempts to retrieve the member from that dict. If the value is not hashable, it then brute-force compares for equality against all existing Enum values, returning the member if finds a match. The relevant code is in lines 468-476 in enum.py:

    try:
        if value in cls._value2member_map_:
            return cls._value2member_map_[value]
    except TypeError:
        # not there, now do long search -- O(n) behavior
        for member in cls._member_map_.values():
            if member._value_ == value:
                return member
    raise ValueError("%r is not a valid %s" % (value, cls.__name__))
    

    So it appears as though the designers of enum.py wanted to have a quick lookup when getting Enums by value, but still wanted to give the flexibility of having mutable values for Enum values (even though I still can't think of a reason why someone would want that in the first place).