Introducing an Enum choice field for Django

Radoslav Georgiev
Jul 30, 2019
Categories:Django

Motivation

In a lot of Django projects, we use choice fields.

A typical model may look like this:

class SomeModelWithChoices(models.Model):
    OK = 'ok'
    PENDING = 'pending'
    FAILED = 'failed'

    CHOICES = (
        (OK, 'Ok'),
        (PENDING, 'Pending'),
        (FAILED, 'Failed'),
    )

    status = models.CharField(max_length=255, choices=CHOICES, default=PENDING)

Usually, the human-readable part is constructed on the frontend, so we just get rid of it:

class SomeModelWithChoices(models.Model):
    OK = 'ok'
    PENDING = 'pending'
    FAILED = 'failed'

    CHOICES = (
        (OK, OK),
        (PENDING, PENDING),
        (FAILED, FAILED),
    )

    status = models.CharField(max_length=255, choices=CHOICES, default=PENDING)

That’s fine.

Where things start to get messy is if we have more than 1 choice field in a model.

For example:

class SomeModelWithChoices(models.Model):
    OK = 'ok'
    PENDING = 'pending'
    FAILED = 'failed'

    CHOICES_A = (
        (OK, OK),
        (PENDING, PENDING),
        (FAILED, FAILED),
    )

    WAITING = 'waiting'
    CANCELLED = 'cancelled'
    READY = 'ready'

    CHOICES_B = (
        (WAITING, WAITING),
        (CANCELLED, CANCELLED),
        (READY, READY),
    )

    status_A = models.CharField(max_length=255, choices=CHOICES_A, default=PENDING)
    status_B = models.CharField(max_length=255, choices=CHOICES_B, default=WAITING)

Even with 2 choice fields, this becomes unreadable.

So the natural progression is to extract constants & add small layer of abstraction:

def get_choices(constants_class: Any) -> List[Tuple[str, str]]:
    return [
        (value, value)
        for key, value in vars(constants_class).items()
        if not key.startswith('__')
    ]


class StatusAConstants:
    OK = 'ok'
    PENDING = 'pending'
    FAILED = 'failed'


class StatusBConstants:
    WAITING = 'waiting'
    CANCELLED = 'cancelled'
    READY = 'ready'


class SomeModelWithChoices(models.Model):
    status_A = models.CharField(
        max_length=255,
        choices=get_choices(StatusAConstants),
        default=StatusAConstants.PENDING
    )
    status_B = models.CharField(
        max_length=255,
        choices=get_choices(StatusBConstants),
        default=StatusBConstants.WAITING
    )

Few things we noticed with this approach:

So why not build something that uses Enums? Well, we did just that.

We created a small layer on top of models.CharField with choices, which uses Python’s enum.Enum as a source.

Here’s the example above, using the EnumChoiceField:

class StatusAEnum(Enum):
    OK = 'ok'
    PENDING = 'pending'
    FAILED = 'failed'


class StatusBEnum(Enum):
    WAITING = 'waiting'
    CANCELLED = 'cancelled'
    READY = 'ready'


class SomeModelWithChoices(models.Model):
    status_A = EnumChoiceField(
        StatusAEnum,
        default=StatusAEnum.PENDING
    )
    status_B = EnumChoiceField(
        StatusBEnum,
        default=StatusBEnum.WAITING
    )

2 quick wins:

Dogfooding

One very important thing that we decided to do for our open source projects is to dog food them.

Or in other words – use them as much as possible. This will force us to fix bugs & provide better support for everything we release.

We are currently going through our projects & integrating django-enum-choices, which actually led us to solving few very interesting cases.

Technical details

As mentioned, we faced interesting challenges, while developing the library.

Vasil Slavov, who did the majority of the work on the library, will follow up with a blog article explaining everything in more details.

Until then, check the examples in the GitHub repo & also consider using this in your projects.

As always, feedback is welcomed!

Open source

As mentioned in our previous open source article, we want to be active on all 3 fronts:

This is a step towards one of the fronts. More – coming soon.