September 16, 2013

As you probably already know, enums are awesome because they allow you to refer to a set of items in a canonical way without having to worry too much about how the items are actually represented. However, this awesomeness is not available out of the box in Python 2.x. Luckily, starting in Python 3.4, an enum implementation is one of the included batteries. Since we're running Python 2.7.x at Hearsay Social, we can't take advantage of the built-in enum library in Python 3.4. Also, there's some functionality that we require that's not part of Python 3.4 enum library and is not provided by the existing enum libraries on the Python Package Index, so we decided to roll our own.

Note: We didn't extend the backported enum34 library since we had already written our own enum library before PEP 435 was created. Luckily, our implementation is similar to enums in Python 3.4 so it should be fairly straightforward to depend on enum34. Feel free to send us a PR to make this change :)


We wrote our own enum library because none of the existing Python enum implementations support all of the following features:

  • Static code analysis (i.e. PyLint) to help find typos
  • Nesting enums
  • Built-in internationalization support
  • Identity comparison

So the problem with existing enum implementations is that they weren't designed with nested enums nor internationalization in mind. To clarify, a nested enum is the ability to associate enum values from one enum to enum values from another enum.

Take a look the 2 implementations of CardSuites below. The first implementation uses Enum from the enum34 package and the second implementation uses RichEnum from our own richenum package.

import gettext
    _ = gettext.translation("enums", fallback=True).ugettext

    from enum import Enum  # enum34 package

    class FMLCardColors(Enum):
        RED = "red"
        BLACK = "black"

    class FMLCardSuites(Enum):
        CLUBS = "clubs"
        DIAMONDS = "diamonds"
        HEARTS = "hearts"
        SPADES = "spades"

    FMLCardSuitesDisplay = {
        FMLCardSuites.CLUBS: _("clubs"),
        FMLCardSuites.DIAMONDS: _("diamonds"),
        FMLCardSuites.HEARTS: _("hearts"),
        FMLCardSuites.SPADES: _("spades"),
    }

    FMLCardSuiteToCardColor = {
        FMLCardSuites.CLUBS: FMLCardColors.BLACK,
        FMLCardSuites.DIAMONDS: FMLCardColors.RED,
        FMLCardSuites.HEARTS: FMLCardColors.RED,
        FMLCardSuites.SPADES: FMLCardColors.BLACK,
    }

    FMLCardColorsDisplay = {
        FMLCardColors.BLACK: _("black"),
        FMLCardColors.RED: _("red"),
    }

    print FMLCardSuitesDisplay[FMLCardSuites.DIAMONDS]  # prints "diamonds"
    print FMLCardColorsDisplay[FMLCardSuiteToCardColor[FMLCardSuites.DIAMONDS]]  # prints "black"
    print [k for k, v in FMLCardSuiteToCardColor.iteritems() if v == FMLCardColors.BLACK]  # prints a list of FMLCardSuites that are black
import gettext
    _ = gettext.translation("enums", fallback=True).ugettext

    from richenum import RichEnumValue, RichEnum

    class _CardColors(RichEnumValue):
        pass

    class CardColors(RichEnum):
        RED = _CardColors(canonical_name="red", display_name=_("red"))
        BLACK = _CardColors(canonical_name="black", display_name=_("black"))

    class _CardSuites(RichEnumValue):
        def __init__(self, canonical_name, display_name, color):
            self.color = color
            super(_CardSuites, self).__init__(canonical_name, display_name)

    class CardSuites(RichEnum):
        CLUBS = _CardSuites(canonical_name="clubs", display_name=_("clubs"),
                            color=CardColors.BLACK)
        DIAMONDS = _CardSuites(canonical_name="diamonds", display_name=_("diamonds"),
                               color=CardColors.RED)
        HEARTS = _CardSuites(canonical_name="hearts", display_name=_("hearts"),
                             color=CardColors.RED)
        SPADES = _CardSuites(canonical_name="spades", display_name=_("spades"),
                             color=CardColors.BLACK)

        @classmethod
        def from_color(cls, color):
            return [e for e in cls if e.color == color]

    print CardSuites.DIAMONDS.display_name  # prints "diamonds"
    print CardSuites.DIAMONDS.color.display_name  # prints "black"
    print CardSuites.from_color(CardColors.BLACK)  # prints a list of CardSuites that are black

As you can see, both implementations offer similar functionality. Both the Enum in enum34 and RichEnum use metaclasses to create the enum class, so you can run static analysis programs like PyLint on your code to check for errors. The usage of metaclasses also allows for comparison by identity.

However, one big difference is that the RichEnum implementation is easier to maintain and allows for cleaner code. As demonstrated in the first implementation, if you want to associate multiple attributes/values to a single enum (i.e. nest enums), then you need to create and maintain another structure per additional attribute/value to map the enum to. It's easier to add a new attribute to each enum value than to add another dictionary that maps all the enum values to the new attribute. Since associating enums is so simple, adding classmethods to get enums based on a related enum is also straightforward. RichEnum also has built-in support for internationalization since it forces you to set a display_name upfront.

Sometimes we want to specify an ordering for our RichEnums. That's where OrderedRichEnum comes in.

Using the implementation of CardSuites above:

print list(suite.display_name for suite in CardSuites)  # prints [u'hearts', u'spades', u'clubs', u'diamonds']

With OrderedRichEnum, we can control the order in which the enum values are returned during iteration.

import gettext
    _ = gettext.translation("enums", fallback=True).ugettext

    from richenum import OrderedRichEnumValue, OrderedRichEnum

    class _OrderedCardColors(OrderedRichEnumValue):
        pass

    class OrderedCardColors(OrderedRichEnum):
        RED = _OrderedCardColors(index=1, canonical_name="red", display_name=_("red"))
        BLACK = _OrderedCardColors(index=2, canonical_name="black", display_name=_("black"))

    class _OrderedCardSuites(OrderedRichEnumValue):
        def __init__(self, index, canonical_name, display_name, color):
            self.color = color
            super(_OrderedCardSuites, self).__init__(index, canonical_name, display_name)

    class OrderedCardSuites(OrderedRichEnum):
        CLUBS = _OrderedCardSuites(index=1, canonical_name="clubs", display_name=_("clubs"),
                                   color=OrderedCardColors.BLACK)
        DIAMONDS = _OrderedCardSuites(index=2, canonical_name="diamonds", display_name=_("diamonds"),
                                      color=OrderedCardColors.RED)
        HEARTS = _OrderedCardSuites(index=3, canonical_name="hearts", display_name=_("hearts"),
                                    color=OrderedCardColors.RED)
        SPADES = _OrderedCardSuites(index=4, canonical_name="spades", display_name=_("spades"),
                                    color=OrderedCardColors.BLACK)

    print list(suite.display_name for suite in OrderedCardSuites)  # prints [u'clubs', u'diamonds', u'hearts', u'spades']

Again, adding support for ordering enum values to the naive enum implementation would be annoying and a pain to maintain. It's easier to add an attribute than to add another dictionary.

Stay tuned for part 2: RichEnums and OrderedRichEnums in Django!



blog comments powered by Disqus