Exploring Python’s new Enum type

The first alpha for Python 3.4 was released yesterday [link]. Two of the major changes to 3.4 were new additions to the standard library: functools.singledispatch and enum.Enum.

Enumerations (Enums) are a way to define a finite set of cases or states with no programmatic ordering. For example, we could have an Enum class with members ["apple", "banana", and "orange"], but it wouldn’t make sense to have an Enum class with members [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] because these have a programatic ordering and are better expressed as integers. Enums have been around in other languages (like C#) for years. There are many times when it’s useful to have a variable representing a particular state, and Python developers have had to find ways around it. Tkinter, for example, has N, S, W, E as cardinal directions used for anchoring widgets. These are stored as integers.

Up until Python 3.4, there were two prominent ways of implementing Enums: global constants, and class variables. Module-level enumeration states are almost always stored as globals. For example:

>>> RED, GREEN, BLUE = range(3)
>>> ORANGE, BANANA, APPLE = range(3)

This works well enough, although I’ve intentionally provided an example where it causes problems. Is ORANGE a color or a fruit?

>>> color = RED
>>> color == RED
True
>>> color == ORANGE
True
>>> color + GREEN == BANANA
True
>>> # Completely nonsensical

Another option is to use classes to get around this:

>>> class Color(object):
        red = 1
        green = 2
        blue = 3

>>> class Fruit(object):
        orange = 1
        banana = 2
        apple = 3

This sort of deal with the ambiguity of the word orange:

>>> color = Color.red
>>> color == Fruit.orange # no one would make this comparison
True

So at least now the members are assigned to a class rather than just global integers. But comparisons between makeshift enum classes still give undesirable results, and we can still perform integer operations on them.

Enter the enum module.

>>> # The new way of doing things:
>>> from enum import Enum
>>> Color = Enum("Color", "red green blue orange black")
>>> Color.red

>>> Color.red + Color.green
Traceback (most recent call last):
  File "<pyshell#16>", line 1, in 
    Color.red + Color.green
TypeError: unsupported operand type(s) for +: 'Color' and 'Color'
>>> # That makes a lot more sense!

This is a huge improvement over the old way of implementing Enums. We can’t add enum members – not without defining a special __add__ method for our class, anyway! However, this is certainly possible, if you want to define a custom __add__ so that Color.red plus Color.blue is Color.purple. Also, Color.red won’t evaluate as equal to anything but itself, regardless of what its internal integer value is.

Wait, its what? Now that we have a dedicated Enum class, why do we need integer values associated with enumeration members? The main reason is for backwards compatibility. Some code relies on modules’ integer values their enum members. In fact, the enum module also provides an IntEnum class, where members can be treated as ints, specifically to support backwards compatibility. Another benefit of having integer values associated with enum members is the ability to include aliases, such as aubergine and eggplant in this example:

>>> class Food(Enum):
        apple = 1
        banana = 2
        orange = 3
        aubergine = 4
        eggplant = 4  # Synonyms can be aliases
        def eat(self):
            print "nom!"

>>> Color.orange == Food.orange
False
>>> Color.orange.name == Food.orange.name
True
>>> Color.red == Food.apple
False
>>> Color.red.value == Food.apple.value
True
>>> for food in Food:
        print(food)
Food.apple
Food.banana
Food.orange
Food.aubergine
Food.eggplant
>>> type(Food.eggplant)
<enum 'Food'>
>>> isinstance(Food.eggplant, int)
False
>>> Food.banana.eat()
nom!

This offers a much more intuitive way to deal with Enums than we have in python 2.7.

Takeaways

Python 3.4′s Enum implementation is definitely better than what we have been using. However, it still relies on externally associating enumeration members with integers. This is mainly because the Enum class is long overdue, and there have been so many workarounds implemented. Also, there’s no reason the enum module can’t be ported to Python 2.7, other than that the core developers really want us to migrate to 3K. I don’t blame them – it has a lot to offer. But there just isn’t enough traction yet, and it may be worth it to include Enum in the Python 2.7 standard library. Or perhaps as a 3rd party module… Thoughts?

Update: Redditor eliben commented that there is an official backport for 2.4-3.3.

4 Comments

  1. >But there just isn’t enough traction yet,

    It’s been four years! There doesn’t need to be “traction” – there just needs to be a recognition that there’s a new version and you’re supposed to be using it, period. The “D” in BDFL is there for a reason. :-)

    Continuing to support, use, look at, think about or even speak out loud Python 2.x hurts the language. It discourages library maintainers from doing the necessary work of porting, which hinders adoption, which hinders porting, etc. Python 2.x has already been assigned an end of support date and there’s a PEP insisting that there will be no more versions of 2.x. Backporting any more features would lead some people to conclude that even these moves aren’t serious. One library maintainer has already told me, “Why should I port to Python 3? Python 2 is going to be supported FOREVER!” I don’t think we want to do anything else to encourage that sort of attitude.

    Delphi went through this kind of thing, but instead of supporting old versions they just kept er, forward-porting old constructs to the new language (seriously, there are some things there for Turbo Pascal compatibility!) Delphi right now has at least SIX ways to read/write a file, all with their own quirks (some don’t support Unicode, etc.) The tragic result of this is that instead of looking upon this as a grace period to convert their software to new standards, many users took it as a sign that they didn’t need to do anything and could just keep their old software as is/keep using out-of-date constructs (same situation as Python, different means). The result of this is that there are many people who as of today haven’t even ported their code to Unicode!!! Instead they “ANSIfied” it, which meant that instead of porting the code to Unicode they made changes so that it would keep working with ASCII under the new compiler. :-( Now Delphi is moving to LLVM and people are throwing fits and bordering on open revolt, screaming “We don’t have time to change!” (because they didn’t use the time they were given).

    I’m watching Python’s potential future unfold in the Delphi world right now. Any effort put into 2.x takes resources away from new development and aids in fragmenting the language. The longer porting to 3.x is put off, the more code will have to be ported… making the job bigger… making one more inclined to put it off… etc. I’m still seeing new libraries today introduced without support for Python 3, which is just a crying shame and people not making the effort to port is directly to blame. I hope Guido’s learned a lesson about being too helpful in regards to backwards compatibility in the future. We don’t want our language to fragment in two… it’s time to move on!

Leave a Reply

Your email address will not be published. Required fields are marked *

     

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>