Need help with your Django project?
Check our django servicesDisclaimer
Starting with version 3.0, Django started supportingEnumerations for model field choices
and we recommend using this as a native Django feature, instead of ourdjango-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.