Many python libraries use magic string constants to represent enums. The fact that enums exist at all, in any language, is evidence of their superiority in these use cases: it’s more discoverable, better for type checking, easier to guarantee the interface, and easier to test. Python considers enums worthwhile, which is why they’re in the standard library, but in downstream libraries it’s much more common to see magic string constants. In the standard library itself, this pattern is also common, but in much of the core there is good reason for that (things which happen before the import
machinery has kicked in, or where you want to avoid importing enum
and its dependencies), so I’m happy to let that slide.
Downstream libraries, however, do not have a consistent upgrade path to go from magic strings to enums. Having to support both the new enum and the old string all the way through their library is a pain, and involves double the equality checks. For some libraries, there are so many code snippets available in so many places that you could never get rid of the magic string interface.
For wrapping underlying libraries which use integers for enums, there is the IntEnum
and IntFlag
: these come with a warning against their use because the integers don’t semantically mean anything, they’re just a convenient crutch for languages without good enum support. But they prove that the python standard library sees the value in wrapping unergonomic, undiscoverable constants in enums, where they’re being used to semantically represent enums anyway.
An Enum
subclass which also subclasses str
would provide one (“and preferably only one”) way forward, where new code and documentation could refer to the enum and old code/examples would still work. Compatibility would be maintained (even including where those magic strings are passed through to underlying libraries, based on my experience with rust/pyo3), and everyone benefits from the advantages of enums.
Such a strategy is already possible. Several downstream libraries include their own implementation of StrEnum. The code required is very small, which means many would prefer not to include another dependency for it: this means dozens of different implementations with slightly different features and inconsistencies. It also means many people wasting a lot of time and space. The code being so small means that very little API surface/ maintenance overhead would be added to the standard library.
Here is the example I tend to use (originally a fork of the unmaintained StrEnum):
import enum
class StrEnum(str, enum.Enum):
def __new__(cls, *args):
for arg in args:
if not isinstance(arg, (str, enum.auto)):
raise TypeError(
"Values of StrEnums must be strings: {} is a {}".format(
repr(arg), type(arg)
)
)
return super().__new__(cls, *args)
def __str__(self):
return self.value
# The first argument to this function is documented to be the name of the
# enum member, not `self`:
# https://docs.python.org/3.6/library/enum.html#using-automatic-values
def _generate_next_value_(name, *_):
return name
I believe it to be more complete than StrEnum and fastapi_utils.enums.StrEnum, and more standard/ lower maintenance than AnyStrEnum.
tl;dr Python already accepts that enums are worthwhile, lots of people use magic string constants where they should use enums, a built-in StrEnum would provide a consistent and easy upgrade path where everyone benefits.