String-based enum in Python
Question:
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
)?
Answers:
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
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 int
s 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.
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
Simply use .value :
MyEnum.state1.value == 'state1'
# True
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.
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', # notice the trailing comma
SOUTH = 'south'
print(Directions.NORTH)
>>> north
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.
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
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
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
)?
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
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 int
s 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.
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
Simply use .value :
MyEnum.state1.value == 'state1'
# True
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.
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', # notice the trailing comma
SOUTH = 'south'
print(Directions.NORTH)
>>> north
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.
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
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