35

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?

2
  • Isn't using an enum to hold any meaningful data completely break the fundamental property of an enum? That it's value is completely arbitrary? If it's being used to form part of a key, it ceases to be an enum and be, well, part of a key, not an enum. Commented Apr 1, 2019 at 12:20
  • 1
    @MikeyB you seem to be confusing the computer science concept of an enumerated type with the Python data type enum.Enum. The Python data type does allow the storage of meaningful data, as do similar "enum" types in the majority of other programming languages, and the use of this capability is a very common practice. If you dislike the association between the concept and the data type due to their similar naming, you could just read the title of the question as "a data type" rather than "an Enum". Commented Apr 2, 2019 at 3:01

7 Answers 7

52

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.

Sign up to request clarification or add additional context in comments.

10 Comments

Thanks for this answer :). I was wondering, how are you supposed to use this missing value method? I mean, I can do MyEnum._missing_value_("value") but I'm not sure if that's the intended way of using it. Using MyEnum['value'] raises an exception
@TomDT: The Enum machinery will automatically call _missing_value_ when an unknown value is looked up. You need to create your own _missing_value_ method to provide any extra search functionality. In the above example, the OP wanted to use Fingers('thumb') to get Fingers.THUMB, which ordinarily wouldn't work since Fingers.THUMB is 1.
Oohhh I see, I was using the brackets [] to access it, but I was supposed to use the parens. Thank you !
@Melvin: Absolutely. Enums are meant to give names to constant values, whether those values are strings, integers, etc..
@Edityouprofile: Without extra work, Fingers.THUMB is not 1 -- it is (1, 'two thumbs').
|
14

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.

from enum import Enum
# We could also do class Finger(IntEnum) its 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'

Below, we have one final test which is quite important, the Finger.__str__ method is automatically created depending on the inheritance class Finger(int, Enum).

If the class was instead defined as class Finger(str, Enum) and

obj = int.__new__(cls, value) became obj = str.__new__(cls, value)

all the checks above would work but this test 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

2 Comments

Please note, I renamed the enum class from Fingers to Finger since Enum classes shouldn't be plural.
This should be the accepted answer as it does not rely on a third-party library and satisfies the constraints of the question.
10

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.

3 Comments

Well spotted, but that's just an accident in the example I have here. In my intended use the strings are longer and not just a lowercase version of the enum names!
does it allow embedded spaces?
@johnywhy Adding a comment to a 7-year-old post is unlikely under normal circumstances to get you a useful answer. But in this case you've lucked out, because I saw it, and you are also out of luck, because the answer is No. There can't be embedded spaces because the attributes of the class Fingers have to be Python identifiers. If you type INDEX FINGER = 2 instead of INDEX = 2 you will get a syntax error at the F after the space.
2

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:\Python\Python35\lib\enum.py", line 241, in __call__
    return cls.__new__(cls, value)
  File "C:\Python\Python35\lib\enum.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.

Comments

2

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

2 Comments

The word you were looking for is pythonic
Seem inconvenient and risky to put the strings in a different location than the enums. Any changes have to be done in both places.
2

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!'

1 Comment

Not sure I agree about use instances of object as the value or custom new() to replace the tuple with an int value
0

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"

1 Comment

Seems like a procedural solution to something that's more akin to a property.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.