Associating string representations with an Enum that uses integer values?

Question:

I’m trying to create an enum that has integer values, but which can also return a display-friendly string for each value. I was thinking that I could just define a dict mapping values to strings and then implement __str__ and a static constructor with a string argument, but there’s a problem with that…

(Under different circumstances I could have just made the underlying data type for this Enum a string rather than an integer, but this is being used as a mapping for an enum database table, so both the integer value and the string are meaningful, the former being a primary key.)

from enum import Enum

class Fingers(Enum):
    THUMB = 1
    INDEX = 2
    MIDDLE = 3
    RING = 4
    PINKY = 5

    _display_strings = {
        THUMB: "thumb",
        INDEX: "index",
        MIDDLE: "middle",
        RING: "ring",
        PINKY: "pinky"
        }

    def __str__(self):
        return self._display_strings[self.value]

    @classmethod
    def from_string(cls, str1):
        for val, str2 in cls._display_strings.items():
            if str1 == str2:
                return cls(val)
        raise ValueError(cls.__name__ + ' has no value matching "' + str1 + '"')

When converting to string, I get the following error:

>>> str(Fingers.RING)
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    str(Fingers.RING)
  File "D:/src/Hacks/PythonEnums/fingers1.py", line 19, in __str__
    return self._display_strings[self.value]
TypeError: 'Fingers' object is not subscriptable

It seems that the issue is that an Enum will use all class variables as the enum values, which causes them to return objects of the Enum type, rather than their underlying type.

A few workarounds I can think of include:

  1. Referring to the dict as Fingers._display_strings.value. (However then Fingers.__display_strings becomes a valid enum value!)
  2. Making the dict a module variable instead of a class variable.
  3. Duplicating the dict (possibly also breaking it down into a series of if statements) in the __str__ and from_string functions.
  4. Rather than make the dict a class variable, define a static method _get_display_strings to return the dict, so it doesn’t become an enum value.

Note that the initial code above and workaround 1. uses the underlying integer values as the dict keys. The other options all require that the dict (or if tests) are defined somewhere other than directly in the class itself, and so it must qualify these values with the class name. So you could only use, e.g., Fingers.THUMB to get an enum object, or Fingers.THUMB.value to get the underlying integer value, but not just THUMB. If using the underlying integer value as the dict key, then you must also use it to look up the dict, indexing it with, e.g., [Fingers.THUMB.value] rather than just [Fingers.THUMB].

So, the question is, what is the best or most Pythonic way to implement a string mapping for an Enum, while preserving an underlying integer value?

Asked By: David Scarlett

||

Answers:

Another solution I came up with is, since both the integers and the strings are meaningful, was to make the Enum values (int, str) tuples, as follows.

from enum import Enum

class Fingers(Enum):
    THUMB = (1, 'thumb')
    INDEX = (2, 'index')
    MIDDLE = (3, 'middle')
    RING = (4, 'ring')
    PINKY = (5, 'pinky')

    def __str__(self):
        return self.value[1]

    @classmethod
    def from_string(cls, s):
        for finger in cls:
            if finger.value[1] == s:
                return finger
        raise ValueError(cls.__name__ + ' has no value matching "' + s + '"')

However, this means that a Fingers object’s repr will display the tuple rather than just the int, and the complete tuple must be used to create Fingers objects, not just the int. I.e. You can do f = Fingers((1, 'thumb')), but not f = Fingers(1).

>>> Fingers.THUMB
<Fingers.THUMB: (1, 'thumb')>
>>> Fingers((1,'thumb'))
<Fingers.THUMB: (1, 'thumb')>
>>> Fingers(1)
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    Fingers(1)
  File "C:PythonPython35libenum.py", line 241, in __call__
    return cls.__new__(cls, value)
  File "C:PythonPython35libenum.py", line 476, in __new__
    raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 1 is not a valid Fingers

An even more complex workaround for that involves subclassing Enum‘s metaclass to implement a custom __call__. (At least overriding __repr__ is much simpler!)

from enum import Enum, EnumMeta

class IntStrTupleEnumMeta(EnumMeta):
    def __call__(cls, value, names=None, *args, **kwargs):
        if names is None and isinstance(value, int):
            for e in cls:
                if e.value[0] == value:
                    return e

        return super().__call__(value, names, **kwargs)

class IntStrTupleEnum(Enum, metaclass=IntStrTupleEnumMeta):
    pass

class Fingers(IntStrTupleEnum):
    THUMB = (1, 'thumb')
    INDEX = (2, 'index')
    MIDDLE = (3, 'middle')
    RING = (4, 'ring')
    PINKY = (5, 'pinky')

    def __str__(self):
        return self.value[1]

    @classmethod
    def from_string(cls, s):
        for finger in cls:
            if finger.value[1] == s:
                return finger
        raise ValueError(cls.__name__ + ' has no value matching "' + s + '"')

    def __repr__(self):
        return '<%s.%s %s>' % (self.__class__.__name__, self.name, self.value[0])

One difference between this implementation and a plain int Enum is that values with the same integer value but a different string (e.g. INDEX = (2, 'index') and POINTER = (2, 'pointer')) would not evaluate as the same Finger object, whereas with a plain int Enum, Finger.POINTER is Finger.INDEX would evaluate to True.

Answered By: David Scarlett

Maybe I am missing the point here, but if you define

class Fingers(Enum):
    THUMB = 1
    INDEX = 2
    MIDDLE = 3
    RING = 4
    PINKY = 5

then in Python 3.6 you can do

print (Fingers.THUMB.name.lower())

which I think is what you want.

Answered By: BoarGules

This can be done with the stdlib Enum, but is much easier with aenum1:

from aenum import Enum

class Fingers(Enum):

    _init_ = 'value string'

    THUMB = 1, 'two thumbs'
    INDEX = 2, 'offset location'
    MIDDLE = 3, 'average is not median'
    RING = 4, 'round or finger'
    PINKY = 5, 'wee wee wee'

    def __str__(self):
        return self.string

If you want to be able to do look-ups via the string value then implement the new class method _missing_value_ (just _missing_ in the stdlib):

from aenum import Enum

class Fingers(Enum):

    _init_ = 'value string'

    THUMB = 1, 'two thumbs'
    INDEX = 2, 'offset location'
    MIDDLE = 3, 'average is not median'
    RING = 4, 'round or finger'
    PINKY = 5, 'wee wee wee'

    def __str__(self):
        return self.string

    @classmethod
    def _missing_value_(cls, value):
        for member in cls:
            if member.string == value:
                return member

1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

Answered By: Ethan Furman

I had same issue where I wanted display strings for a GUI ComboBox (in PyGTK).
I dun’t know about the Pythonicity (is that even a word?) of the solution but I used this:

from enum import IntEnum
class Finger(IntEnum):
    THUMB = 1
    INDEX = 2
    MIDDLE = 3
    RING = 4
    PINKY = 5

    @classmethod
    def names(cls):
        return ["Thumb", "Index", "Middle", "Ring", "Pinky"]

    @classmethod
    def tostr(cls, value):
        return cls.names()[value - cls.THUMB]

    @classmethod
    def toint(cls, s):
        return cls.names().index(s) + cls.THUMB

Using them from your code:

>>> print(Finger.INDEX)
Finger.INDEX
>>> print(Finger.tostr(Finger.THUMB))
Thumb
>>> print(Finger.toint("Ring"))
4
Answered By: mmeisner

Although this is not what OP has asked, but still this is one good option when you do not care if the value is int or not. You can use the value as the human readable string.

Source: https://docs.python.org/3/library/enum.html

Omitting values
In many use-cases one doesn’t care what the actual value of an enumeration is. There are several ways to define this type of simple enumeration:

  • use auto() for the value
  • use instances of object as the value
  • use a descriptive str as the value
  • use a tuple as the value and a custom new() to replace the tuple
    with an int value

Using any of these techniques signifies to the user that these values are not important, and also enables one to add, remove, or reorder members without having to renumber the remaining members.

Whichever method you choose, you should provide a repr() that also hides the (unimportant) value:

class NoValue(Enum):
     def __repr__(self):
         return '<%s.%s>' % (self.__class__.__name__, self.name)

Using a descriptive string
Using a string as the value would look like:

class Color(NoValue):
     RED = 'stop'
     GREEN = 'go'
     BLUE = 'too fast!'

Color.BLUE
<Color.BLUE>
Color.BLUE.value
'too fast!'
Answered By: Neeraj Gulia

The python docs have a somewhat abstract example here, from which I was able to come up with this solution

I have added an explanation inline, as comments.

# we could also do class Finger(IntEnum) it's equivalent.
class Finger(int, Enum):
    def __new__(cls, value, label):
        # Initialise an instance of the Finger enum class 
        obj = int.__new__(cls, value)
        # Calling print(type(obj)) returns <enum 'Finger'>
        # If we don't set the _value_ in the Enum class, an error will be raised.
        obj._value_ = value
        # Here we add an attribute to the finger class on the fly.
        # One may want to use setattr to be more explicit; note the python docs don't do this
        obj.label = label
        return obj

    THUMB = (1, 'thumb')
    INDEX = (2, 'index')
    MIDDLE = (3, 'middle')
    RING = (4, 'ring')
    PINKY = (5, 'pinky')

    @classmethod
    def from_str(cls, input_str):
        for finger in cls:
            if finger.label == input_str:
                return finger
        raise ValueError(f"{cls.__name__} has no value matching {input_str}")

So let’s test it.

In [99]: Finger(1)
Out[99]: <Finger.THUMB: 1>

In [100]: Finger.from_str("thumb")
Out[100]: <Finger.THUMB: 1>

In [101]: Finger.THUMB
Out[101]: <Finger.THUMB: 1>

In [102]: Finger.THUMB.label
Out[102]: 'thumb'

The last test here is quite important, the __str__ method is automatically created depending on the inheritance class Finger(int, Enum).

If this was instead class Finger(str, Enum) and obj = int.__new__(cls, value) became obj = str.__new__(cls, value) all the checks above would work but the call to __str__ would’ve raised an error.

In [103]: f"Finger.THUMB"
Out[103]: '1'

An example of it being used in python3 standard library http.HTTPStatus

Answered By: Mark

Just implement it like the .value property:

from enum import Enum

class SomeEnum(Enum):
    FOO = 0
    BAR = 1

print(SomeEnum.FOO.value)
# prints 0

Now just implement your own str_value property for a string:

class SomeEnum(Enum):
    FOO = 0
    BAR = 1

    @property
    def str_value(self):
        if self.value == 0:
            return "this is foo"

        return "this is bar"
    
print(SomeEnum.BAR.str_value)
# prints "this is bar"
Answered By: u84six
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.