Moving to Django 3.0’s Field.choices Enumeration Types2020-01-27
One of the headline features of Django 3.0 is its Enumerations for model field choices.
They’re a nicer way of defining and constraining model
Previously, Django recommended defining some ‘constants’ on your model class to feed into
You would add these as separate class variables and combine them into a list of choices paired with their display strings:
from django.db import models class Book(models.Model): UNPUBLISHED = "UN" PUBLISHED = "PB" STATUS_CHOICES = [ (UNPUBLISHED, "Unpublished"), (PUBLISHED, "Published"), ] status = models.CharField( max_length=2, choices=STATUS_CHOICES, default=UNPUBLISHED, )
Then your other could use those constants, for example:
unpublished_books = Book.objects.filter(status=Book.UNPUBLISHED)
If you wanted to use the same set of values for multiple models, you would probably move to the module level:
from django.db import models UNPUBLISHED = "UN" PUBLISHED = "PB" STATUS_CHOICES = [ (UNPUBLISHED, "Unpublished"), (PUBLISHED, "Published"), ] class Book(models.Model): status = models.CharField( max_length=2, choices=STATUS_CHOICES, default=UNPUBLISHED, ) class Pamphlet(models.Model): status = models.CharField( max_length=2, choices=STATUS_CHOICES, default=PUBLISHED, )
This leaves a bunch of constants related only by their position in the class or module. It’s a bit against The Zen of Python’s final edict:
Namespaces are one honking great idea – let’s do more of those!
It also leaves us missing some useful functionality. For example there’s no easy way to convert a value into its display label.
Packages like django-choices and django-enumfields exist to solve these problems. I’ve also seen a couple custom implementations of similar functionality on other projects.
Django 3.0 now provides a
Choices class with two subclasses
These extend Python’s
Enum types with extra constraints and functionality to make them suitable for
To convert our example, we want to subclass
TextChoices, because the values are strings stored in a
We then define how names map to values and display labels as class attributes:
from django.db import models class Status(models.TextChoices): UNPUBLISHED = "UN", "Unpublished" PUBLISHED = "PB", "Published" class Book(models.Model): status = models.CharField( max_length=2, choices=Status.choices, default=Status.UNPUBLISHED, ) class Pamphlet(models.Model): status = models.CharField( max_length=2, choices=Status.choices, default=Status.PUBLISHED, )
We can test we converted correctly by checking no migration changes are detected:
$ python manage.py makemigrations --dry-run No changes detected
If we had added, removed, or reordered any members, this would have been detected as changes in the fields.
This is because the migration framework only sees the
choices list generated by
Status.choices, not the enumeration classes.
QuerySet filters can be updated to use the
unpublished_books = Book.objects.filter(status=Status.UNPUBLISHED)
We can also convert values to their display labels easily:
In : book = Book.objects.latest('id') In : Status(book.status) Out: <Status.UNPUBLISHED: 'UN'> In : Status(book.status).label Out: 'Unpublished'
Clearer and cleaner!
I hope this helps you enjoy this new Django 3.0 feature. The Enumeration types documentation covers a few more details and is worth a read.
Thanks to Shai Berger, Nick Pope, Marius Felisiak, Carlton Gibson, and all the others responsible for adding it (ticket #27910).
If your Django project’s long test runs bore you, my book can help.
One summary email a week, no spam, I pinky promise.
- Django’s Field Choices Don’t Constrain Your Data
- A Single File Asynchronous Django Application
- How to Add Database Modifications Beyond Migrations to Your Django Project
© 2020 All rights reserved.