110

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)?

sophros
  • 14,672
  • 11
  • 46
  • 75

8 Answers8

152

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
Guillaume Jacquenot
  • 11,217
  • 6
  • 43
  • 49
sophros
  • 14,672
  • 11
  • 46
  • 75
94

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 Directions(StrEnum):
    NORTH = 'north'
    SOUTH = 'south'

print(Directions.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 Directions(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 URLs(StrEnum):
    GOOGLE = 'www.google.com'
    STACKOVERFLOW = 'www.stackoverflow.com'

print(URLs.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

Taylor D. Edmiston
  • 12,088
  • 6
  • 56
  • 76
Elyasaf755
  • 2,239
  • 18
  • 24
  • 3
    its 3.11 for StrEnum – Rohit Dec 17 '21 at 16:41
  • 2
    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. – Denis Barmenkov Feb 08 '22 at 00:07
  • Doh. Doesn't work in 3.7.3 :( – Mote Zart Apr 22 '22 at 23:09
  • 2
    This should be the correct answer for python>=3.11 – serkef Jan 04 '23 at 12:02
  • Why do you include the trailing comma here: `NORTH = 'north', # notice the trailing comma`? I do not see this trailing comma style used anywhere in the docs. Are you sure this is doing something? (Subclassing `StrEnum` appears to work just fine without such commas.) – Taylor D. Edmiston Jun 15 '23 at 18:05
  • I posted this answer on the same day Python 3.10 was released (and I think before it got released on the same day). Initially, StrEnum was targetted to 3.10, and then postponed to 3.11. I didn't have 3.10, so I just copy-pasted whatever the docs said, because I couldn't test it myself and see if it actually makes any difference . Now, because the target version changed to 3.11, they removed this section from 3.10 docs, and then added a new section in 3.11 docs, where this example is not present. I found a Chinese copy of the old 3.10 docs: https://www.osgeo.cn/cpython/library/enum.html#strenum – Elyasaf755 Jun 16 '23 at 07:05
  • 1
    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. – Elyasaf755 Jun 16 '23 at 07:10
  • We can use `auto()` from `enum` with the strenum package for older python versions, but it will case fold name to lowercase by default. – Dimitri Lesnoff Jul 10 '23 at 08:46
11

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

  • 1
    but then he would still have to call `str()` on the value or not? – Raphael Jul 28 '22 at 11:19
  • 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... https://docs.python.org/3/reference/datamodel.html#object.__str__ – Gabriele Iannetti Jul 29 '22 at 12:49
5

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.

Gloweye
  • 1,294
  • 10
  • 20
  • 5
    `Enum` was created so opaque module constants would not be needed. – Ethan Furman Oct 29 '19 at 14:36
  • 1
    @EthanFurman Then why are logging.DEBUG and friends not deprecated ? – Gloweye Oct 29 '19 at 14:38
  • 2
    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). – Ethan Furman Oct 29 '19 at 14:48
  • 1
    The relevant part of [`PEP 435`](https://www.python.org/dev/peps/pep-0435/#use-cases-in-the-standard-library). – Ethan Furman Oct 29 '19 at 14:49
  • @EthanFurman I recently read the arguments [here](https://mail.python.org/archives/list/python-ideas@python.org/thread/FTGUKZT4TSIGD755UQGUZOCB4VC4UUBN/#376RK7XU36QH3CJQYEVASO7NUXGBCOWJ) about it, where the more known names seemed to be in favor of keeping named constants. Some also for mixins, though less. – Gloweye Oct 29 '19 at 14:56
  • 1
    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. – Ethan Furman Oct 29 '19 at 15:28
  • @EthanFurman I'll be the first to grab Enum for things in a lot of places, but generally only when I (plan to) compare it to another member of the same Enum. Hence the main part of my answer: **Use the right tool for the job**. Question is *specifically* about cross-comparisons, and when those are part of the **design**, I feel Enum's aren't the right tool. But I do fully get where you're coming from. – Gloweye Oct 29 '19 at 15:32
  • I hear what you're saying, but how is that different than comparing one module string constant to another module string constant? – Ethan Furman Oct 29 '19 at 16:12
  • @EthanFurman IMHO, it's about intent. In my mind, Enums are compared against values that you expect an Enum of the same type, and if you want to compare to arbitrary strings, then module constants are more appropriate. There's a hint of type-safety in enums - perhaps my `moduleA` has a `class Mode(Enum):DEBUG=auto();PRODUCTION=auto()` - and that will compare unequal to your `moduleB`'s Mode Enum, even if you have the exact same code building it. If you don't want that type-safety, then perhaps you don't want enums. – Gloweye Oct 30 '19 at 08:23
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

Shital Shah
  • 63,284
  • 17
  • 238
  • 185
4

With auto:

from enum import Enum, auto

class AutoStrEnum(str, Enum):
    """
    StrEnum where 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

class MyEnum(AutoStrEnum):
    STATE_1 = auto()
    STATE_2 = auto()

Try it:

MyEnum.STATE_1 == "STATE_1"  # True
Noam Nol
  • 570
  • 4
  • 11
  • 1
    Usually, I prefer lower case enum: `class AutoStrEnumLCase(str, Enum): def _generate_next_value_(name, start, count, last_values): return name.lower()` – Noam Nol May 14 '23 at 14:16
1

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.

-1

Simply use .value :

MyEnum.state1.value == 'state1'
# True
pierre-vr
  • 19
  • 5