The basics of Enums are explained pretty well here. My goal here is to go over some specific pitfalls and provide some insight into how things work under the hood, without diving into the nitty-gritty details of meta-classes.
Python Enums are not some special language construct, they are made using Python tools available to us, and provided as part of the standard library. In fact, you could create your own Enum if you wanted to. To do that, you'd use metaclasses, which you can also see in the standard library implementation. For now, the takeaway is that Enums don't require any special syntax unlike in some other languages (for example, in c++ you'd use the reserved keyword enum
or enum class
), whereas is python you create them just like a class.
How do Enums work
An Enum is meant to be some limited set of values known statically, before running your code. As previously said, there is no special syntax, You make an enum by inheriting from Enum. Taking an example from the how-to page:
class Weekday(Enum):
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
SUNDAY = 7
Because you inherit from Enum, the way this class is created is significantly altered. As a side note, this is what metaclasses are for, customizing how classes should work, much like how classes describe how objects should work.
Most importantly, a finite set of objects is created, one for each property defined on the class, It is made impossible 1 to ever create any other objects of this class. Where in a regular class, you create an object by calling the class, this gives us a value based lookup for the enum above.
>>> Weekday(5)
<Weekday.FRIDAY: 5>
If we access the object by Weekday.FRIDAY
we get the exact same object. Basically, no matter how we try to access it, only one object was ever created and that object is returned. Keeping this in mind, it makes more sense Enums are compared by identity using is
, as opposed to ==
.
💡 Compare enums by identity (using
is
oris not
).
Enum values
There aren't a whole lot of enforced restrictions on the value given to an Enum, but it is important to keep a few things in mind. Mutables can be used but have some performance implications as some operations get less efficient, one of which is mentioned in the docs. In general, the Enum implementation relies on hashability to do certain things efficiently. With unhashable types, it falls back to a less efficient method. For example, A value based lookup such as in the previous section would take O(n)
time, rather than a simple dict lookup.
With that in mind, and from my own experience of trying to do things that are too complex with Enums, I'd recommend sticking with either simple types (int, str) or frozen dataclasses as the value type for enums. You could use something like a tuple or named tuple as well, but i find frozen dataclasses to almost always be a better alternative (my other blog may provide more insight on this).
💡 Keep your enums simple, use builtin immutables like
int
orstr
, or basic frozen dataclasses.
Enum aliases
One interesting thing happens if you assign the same value for different Enums members:
class Test(Enum):
FOO=1
BAR=1
Rather than making two enum member objects, you'll get a single object and an alias that points to the same object. This can be shown by an identity comparison
>> Test.FOO is Test.BAR
True
This is covered well by the docs, but i'm bringing it up because it's particularly interesting to look at the combination of aliases and mutables. Looking at the following example:
class Test(Enum):
FOO=[]
BAR=[]
Since BAR is just an alias for FOO, you get:
>> Test.FOO.value.append(1)
>>> Test.BAR.value
[1]
This is one more reason to be mindful of using mutables in the context of enums.
💡 If you want to prevent aliases, use the unique decorator on your enum class.
A note on IntEnum, StrEnum and more
Enums are not just an alias to a raw value. Especially when using a type checker, they help in making sure you don't compare apples to oranges. This is because an Enum and its value are separate types, if you want the value of an enum, you use WeekDay.FRIDAY.value
, but you have to be explicit. You can't just pass WeekDay.FRIDAY to a function that accepts integers. On the other hand, the standard library also offer an IntEnum and StrEnum, which blur the lines between value type and enum type. Consider the following example:
class Direction(IntEnum):
UP = 1
DOWN = 2
LEFT = 3
RIGHT = 4
class Color(IntEnum):
RED = 1
GREEN = 2
BLUE = 3
The problem with using IntEnum and StrEnum is that these inherit from int and str, and as a result, a bunch of operations suddenly become possible. For example:
>> Direction.UP + 2
3
>> direction = Direction.DOWN
>> speed = 2
>> speed == direction # Comparing these doesn't make sense, with a regular Enum a type checker will flag this
True
>> Color.RED == Direction.UP # We should compare by identity using `is` instead, but if we do so by mistake this happily passes
True
Also any function that can take an integer now also accepts every IntEnum passed to it.
This is not to say that there aren't ever use-cases for these classes, but most of the time there is a clear separation between an Enum and the internal value that happens to represent it. In this case a regular Enum provides better type safety and prevents some forms of misuse. You have to be explicit about when you want to convert to and from the raw value, and that's a good thing.
💡 Prefer
Enum
overIntEnum
andStrEnum
, as it's more explicit and type-safe when using a type checker.
Final remarks
There is so much more to cover on enums. One of my personal favorites that i haven't mentioned so far is usage of auto, when I really don't care about the value used to represent an Enum. I haven't mentioned it because i did not have any additional insights that aren't covered by the docs.
All that's left is one final piece of general advice: When it comes to Enums, Keep it Simple!
-
That's not entirely true, it has been made impossible through the standard object creation flow, but it is technically still possible, for example through
WeekDay._new_member_
orobject.__new__
. This breaks in various ways, you might be able to manually fix those, but please just don't. ↩
Top comments (0)