167

To encapsulate a list of states I am using enum module:

from enum import Enum

class MyEnum(Enum):
    state1='state1'
    state2 = 'state2'

state = MyEnum.state1
MyEnum['state1'] == state  # here it works
'state1' == state  # here it does not throw but returns False (fail!)

However, the issue is that I need to seamlessly use the values as strings in many contexts in my script, like:

select_query1 = select(...).where(Process.status == str(MyEnum.state1))  # works but ugly

select_query2 = select(...).where(Process.status == MyEnum.state1)  # throws exeption

How to do it avoiding calling additional type conversion (str(state) above) or the underlying value (state.value)?

5
  • 9
    MyEnum.state1.value? Commented Oct 29, 2019 at 13:30
  • 8
    Sorry but this is same as ugly as str(state) to me... Commented Oct 29, 2019 at 13:40
  • What type is Testround.status? Could you make it of type MyEnum? Commented Oct 29, 2019 at 14:25
  • 2
    Your example code 'state1' == state is wrong -- that comparison returns False. Commented Oct 29, 2019 at 14:39
  • 1
    @EthanFurman: it does not throw but indeed the result is far from satisfactory. Thanks! Correcting. Commented Oct 29, 2019 at 15:03

8 Answers 8

216

It seems that it is enough to inherit from str class at the same time as Enum:

from enum import Enum

class MyEnum(str, Enum):
    state1 = 'state1'
    state2 = 'state2'

The tricky part is that the order of classes in the inheritance chain is important as this:

class MyEnum(Enum, str):
    state1 = 'state1'
    state2 = 'state2'

throws:

TypeError: new enumerations should be created as `EnumName([mixin_type, ...] [data_type,] enum_type)`

With the correct class the following operations on MyEnum are fine:

print('This is the state value: ' + state)

As a side note, it seems that the special inheritance trick is not needed for formatted strings which work even for Enum inheritance only:

msg = f'This is the state value: {state}'  # works without inheriting from str
Sign up to request clarification or add additional context in comments.

6 Comments

Please note that a a str mixin may have unintended side effect, e.g. on (de)serialization behaviour; see for example stackoverflow.com/q/65339635/10009545
It's simpler to just subclass StrEnum if you're on Python 3.11+ - docs.python.org/3/howto/enum.html#strenum.
@TaylorD.Edmiston - as Elyasaf755 suggests below in his answer.
I used to do this but had strange behaviour on python 3.10.11, with a function returning an enum object would get deserialised to the string when assigning to a variable. Removing the str subclass fixed it and it seems to work fine without it.
|
178

By reading the documentation (i.e., I didn't try it because I use an older version of Python, but I trust the docs), since Python 3.11 you can do the following:

from enum import StrEnum

class Direction(StrEnum):
    NORTH = 'north'
    SOUTH = 'south'

print(Direction.NORTH)
>>> north

Note that it looks like when subclassing StrEnum, defining the enum fields as single-value tuples will make no difference at all and would also be treated as strings, like so:

class Direction(StrEnum):
    NORTH = 'north',    # notice the trailing comma
    SOUTH = 'south'

Please refer to the docs and the design discussion for further understanding.

If you're running python 3.6+, execute pip install StrEnum, and then you can do the following (confirmed by me):

from strenum import StrEnum

class URL(StrEnum):
    GOOGLE = 'www.google.com'
    STACKOVERFLOW = 'www.stackoverflow.com'

print(URL.STACKOVERFLOW)

>>> www.stackoverflow.com

You can read more about it here.


Also, this was mentioned in the docs - how to create your own enums based on other classes:

While IntEnum is part of the enum module, it would be very simple to implement independently:

class IntEnum(int, Enum): pass This demonstrates how similar derived enumerations can be defined; for example a StrEnum that mixes in str instead of int.

Some rules:

When subclassing Enum, mix-in types must appear before Enum itself in the sequence of bases, as in the IntEnum example above.

While Enum can have members of any type, once you mix in an additional type, all the members must have values of that type, e.g. int above. This restriction does not apply to mix-ins which only add methods and don’t specify another type.

When another data type is mixed in, the value attribute is not the same as the enum member itself, although it is equivalent and will compare equal.

%-style formatting: %s and %r call the Enum class’s str() and repr() respectively; other codes (such as %i or %h for IntEnum) treat the enum member as its mixed-in type.

Formatted string literals, str.format(), and format() will use the mixed-in type’s format() unless str() or format() is overridden in the subclass, in which case the overridden methods or Enum methods will be used. Use the !s and !r format codes to force usage of the Enum class’s str() and repr() methods.

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

11 Comments

Let's use auto generated enum values in serialization. Then it could be break easily by updating to buggy 'strenum' version. It's not worth it.
Doh. Doesn't work in 3.7.3 :(
This should be the correct answer for python>=3.11
Anyways, looks like you are correct andthe trailing comma doesn't make any djfference. I think what they wanted to note, is that unlike other classes, it doesn't matter if the enum fields are defined as single-value tuples or not, they will be treated as strings anyway? Who knows. Thanks for your comment, I will update the answer.
The trailing comma on Enum definitions is a common careless mistake @TaylorD.Edmiston, possibly exacerbated by copy paste of values from somewhere or noobs more used to other languages. Perhaps answer identifies that this common mistake is rendered harmless by the StrEnum implementation (although I would argue that doing so is unPythonic)?
|
25

what is wrong with using the value?

Imho, unless using Python version 3.11 with StrEnum I just override the __str__(self) method in the proper Enum class:

class MyStrEnum(str, Enum):

    OK     = 'OK'
    FAILED = 'FAILED'

    def __str__(self) -> str:
        return self.value

Best

3 Comments

but then he would still have to call str() on the value or not?
No, when you are working with strings, it is not necessary to explicit call str(), since Python will return the internal string representation of that object then... docs.python.org/3/reference/datamodel.html#object.__str__
Yes, I think this simple expedient helps with the awkward verbosity of Enum when used with str. There feels to me like there's something fundamentally wrong with the old Enum design that I can't put my finger on. It's almost like Enum helps with restricting to a specific list but after it's done that it's more convenient to use strings than the actual Enum. I'm looking forward to trying 3.11 behaviours but I'm currently on 3.10.
22

StrEnum with auto

In many cases you can save yourself some work and use auto().

Since Python 3.11:

Define enum using auto:

from enum import StrEnum, auto

class Color(StrEnum):
    RED = auto()
    BLUE = auto()

Use the enum:

Color.BLUE == "blue"  # True
str(Color.BLUE)  # "blue"

By default, auto results in the lower-cased member name as the value. It is possible to define a custom enum class to get the original member name:

from enum import StrEnum, auto

class OCaseStrEnum(StrEnum):
    """
    StrEnum where enum.auto() returns the original member name, not lower-cased name.
    """
    @staticmethod
    def _generate_next_value_(name: str, start: int, count: int, last_values: list) -> str:
        return name

class Color(OCaseStrEnum):
    RED = auto()
    BLUE = auto()

Color.BLUE == "BLUE"  # True

Before Python 3.11:

Define enum using auto:

from enum import auto
from my_utils import AutoStrEnum

class Color(AutoStrEnum):
    RED = auto()
    BLUE = auto()

Use the enum:

Color.BLUE == "BLUE"  # True
str(Color.BLUE)  # "BLUE"

AutoStrEnum definition:

from enum import Enum

class AutoStrEnum(str, Enum):
    """
    StrEnum where enum.auto() returns the field name.
    See https://docs.python.org/3.9/library/enum.html#using-automatic-values
    """
    @staticmethod
    def _generate_next_value_(name: str, start: int, count: int, last_values: list) -> str:
        return name
        # Or if you prefer, return lower-case member (it's StrEnum default behavior since Python 3.11):
        # return name.lower()

    def __str__(self) -> str:
        return self.value  # type: ignore

StrEnum can be used as a key:

>>> d = {Color.BLUE: 10, "red": 20}
>>> d["blue"], d[Color.BLUE], d["red"], d[Color.RED]
(10, 10, 20, 20)

repr of StrEnum is different from regular string (see PEP 663):

>>> repr("blue")
"'blue'"
>>> repr(Color.BLUE)
"<Color.BLUE: 'blue'>"

Comments

5

If associated string values are valid Python names then you can get names of enum members using .name property like this:

from enum import Enum
class MyEnum(Enum):
    state1=0
    state2=1

print (MyEnum.state1.name)  # 'state1'

a = MyEnum.state1
print(a.name)  # 'state1'

If associated string values are arbitrary strings then you can do this:

class ModelNames(str, Enum):
    gpt2 = 'gpt2'
    distilgpt2 = 'distilgpt2'
    gpt2_xl = 'gpt2-XL'
    gpt2_large = 'gpt2-large'

print(ModelNames.gpt2) # 'ModelNames.gpt2'
print(ModelNames.gpt2 is str) # False
print(ModelNames.gpt2_xl.name) # 'gpt2_xl'
print(ModelNames.gpt2_xl.value) # 'gpt2-XL'

Try this online: https://repl.it/@sytelus/enumstrtest

Comments

4

While a mixin class between str and Enum can solve this problem, you should always also think about getting the right tool for the job.

And sometimes, the right tool could easily just be a MODULE_CONSTANT with a string value. For example, logging has a few constants like DEBUG, INFO, etc with meaningful values - even if they're ints in this case.

Enums are a good tool and I often use them. However, they're intended to be primarily compared against other members of the same Enum, which is why comparing them to, for example, strings requires you to jump through an additional hoop.

11 Comments

Enum was created so opaque module constants would not be needed.
@EthanFurman Then why are logging.DEBUG and friends not deprecated ?
They wouldn't be deprecated -- they would be replaced by a corresponding IntEnum. It is standard policy to keep the stdlib as stable as possible, which means not rewriting it wholesale to take advantage of every new feature. So far http, socket, and re module constants have been replaced (and maybe a couple others I don't remember at the moment).
The relevant part of PEP 435.
Wow, it feels like a long time since we had that conversation! The main points there relate to my earlier comment: not rewriting the stdlib without good reason. The case for magic strings is even harder to make, because the strings are usually self-explanatory. The modules with the best chance of having Enum conversions are user-facing with integer constants (such as re and http). Also, a huge reason to not convert specific portions of the stdlib is if it's used before Enum can be imported. These are all reasons that do not affect code outside the stdlib.
|
4

If you want to work with strings directly, you could consider using

MyEnum = collections.namedtuple(
    "MyEnum", ["state1", "state2"]
)(
    state1="state1", 
    state2="state2"
)

rather than enum at all. Iterating over this or doing MyEnum.state1 will give the string values directly. Creating the namedtuple within the same statement means there can only be one.

Obviously there are trade offs for not using Enum, so it depends on what you value more.

Comments

0

I prefer writing this complex util function for no reason to use yaml as enum source. It gives me flexibility at a cost 🤷‍♂️

import yaml
from enum import Enum

def enuml(yaml_string: str) -> type[Enum]:
    class StrValueEnum(str, Enum):
        def __str__(self):
            return self.value
        
        def __repr__(self):
            return f"{self.__class__.__name__}.{self.name}"

    data = yaml.safe_load(yaml_string)
    if not data:
        raise ValueError("Invalid YAML string")

    enum_name, values = next(iter(data.items()))
    if not isinstance(values, list) or not all(isinstance(value, str) for value in values):
        raise ValueError("Invalid YAML string")

    enum_class = StrValueEnum(enum_name, {value: value for value in values})
    
    # Save the Enum to the global namespace
    globals()[enum_name] = enum_class

    return enum_class

Usage:

enuml(
"""
Moto:
  - yamaha
  - suzuki
  - honda
"""
)

Moto.honda == "honda" # True

Comments

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.