Tom Wojcik personal blog

StrEnum(str, enum.Enum) is not backwards compatible starting with Python 3.11

☕️ 2 min read

There 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"