HackSoft logo
  • Approach
  • Case Studies
  • Team
  • Company
  • Services
      Custom Software Development ConsultingAI Integrations
  • Solutions
  • Open Source
  • Blog

Need help with your Django project?

Check our django services

Introducing an Enum choice field for Django

Radoslav Georgiev
Jul 30, 2019
Categories:Django

Disclaimer

Starting with version 3.0, Django started supporting Enumerations for model field choices and we recommend using this as a native Django feature, instead of our django-enum-choices library.

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 a 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 solve a 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.

Need help with your Django project?

Check our django services
HackSoft logo
Your development partner beyond code.