Introducing an Enum choice field for 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:

  • We always specify max_length=255 and don’t actually count the proper max length.
  • If we want to iterate over all possible constant choices, we need to use get_choices again.
  • We are replicating Enums & Python has enum.Enum.

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:

  • max_length is calculated automatically, taking the longest value.
  • We don’t need the get_choices util every time we have to iterate over all enum values.

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:

  • Supporting open source libraries we use.
  • Contributing to open source libraries we use.
  • Producing open source from our daily work.

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

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.