StrEnum(str, enum.Enum) is not backwards compatible starting with Python 3.11
• • ☕️ 2 min readThere are multiple ways of defining constants in Python. I like static typing, so creating enums that inherit from both, str
and enum.Enum
, was always giving me the desired flexibility.
import enum
class Foo(str, enum.Enum):
bar = 'bar'
def baz(foo: Foo) -> str:
return f"prefix_{foo}"
assert baz(Foo.bar) == "prefix_bar"
This snippet is working as expected (in Python < 3.11). Function baz
returns a string, and it’s able to cast Foo.bar
to string because it inherits from str
. Static typing is also satisfied, IDE will not complain.
But… in Python 3.11 we will see an AssertionError
, because baz
returns prefix_Foo.bar
.
In the release notes for Python 3.11 we can see
Changed Enum.format() (the default for format(), str.format() and f-strings) of enums with mixed-in types (e.g. int, str) to also include the class name in the output, not just the member’s key. This matches the existing behavior of enum.Enum.str(), returning e.g. “AnEnum.MEMBER” for an enum AnEnum(str, Enum) instead of just 'MEMBER’.
It’s a tricky thing to spot because Foo.bar
returns the same value. We can see it works differently only when
using it to format a string.
Inheriting from both str
and enum.Enum
is kind of hacky, so I’m glad an official support for StrEnum
was introduced - enum.StrEnum
.
The bad news it that from now on, if I want to support Python versions below 3.11 and above, I need to rely on this monstrosity.
try:
# breaking change introduced in python 3.11
from enum import StrEnum
except ImportError: # pragma: no cover
from enum import Enum # pragma: no cover
class StrEnum(str, Enum): # pragma: no cover
pass # pragma: no cover
Of course one could check Python version instead of handling ImportError
, but I prefer this approach.
Full backwards-compatible Python 3.x example
try:
# breaking change introduced in python 3.11
from enum import StrEnum
except ImportError: # pragma: no cover
from enum import Enum # pragma: no cover
class StrEnum(str, Enum): # pragma: no cover
pass # pragma: no cover
class Foo(StrEnum):
bar = 'bar'
def baz(foo: Foo) -> str:
return f"prefix_{foo}"
assert baz(Foo.bar) == "prefix_bar"