Django Choices Best Practices

Models in Django define entities and their fields (columns) in the database. These columns represent the Django Field class with built-in arguments we could use. This article will cover best practices of using choices argument in Django fields.

What are Django field choices?
Django field choices allow us to define preexisting data and use it without defining the additional model. This data can be forwarded using the iterable of tuples containing two items or by subclassing the Choices class.

There are multiple ways of defining Field choices:

  • Using sequence – an iterable that support element access using an integer.
  • Extending Choices class – enumeration types.
  • Using third-party libraries

Firstly, we’ll cover how to set choices argument using sequence.

Django Field Choices using Sequence

Python sequence is iterable that can be used to define Django field choices. These sequences come in different shapes and forms, most common being lists and tuples.

We’ll use the Book model with three possible authors in the following examples.

from django.db import models

class Book(models.Model):
   AUTHOR_CHOICES = [
       ('WS', 'William Shakespeare'),
       ('MC', 'Miguel de Cervantes'),
       ('AC', 'Agatha Christie'),
   ]

   title = models.CharField(max_length=512)
   author = models.CharField(max_length=512, choices=AUTHOR_CHOICES)

As expected, this creates a Book table with id (integer), title (varchar), and author (varchar) columns in the database.

The first real changes we can see are in the Django Admin page panel after registering our model.
Django Admin, when creating authors, offers only authors we defined in our AUTHOR_CHOICES sequence.

Django model choices in Admin panel without grouping.

After choosing ‘William Shakespeare’ and saving it to the database, the database looks like this:

idtitleauthor
1HamletWS

The author column is saved as the first element from the tuple. The second element is a human-readable representation of a given value.

Using Python console to add authors, we can still add authors not defined in the AUTHOR_CHOICES variable.

>>> Book(title="Harry Potter", author="J. K. Rowling").save()

This is valid, and no exception is raised. However, although this works, this should be avoided because if your project contains more than already defined authors, consider using a many-to-one relationship. Many to one relationship resolve this obstacle by creating a foreign key connection to another table in the database.

Django documentation suggests using constants to define values ​​of the sequence.

Code with constants would look like this:

from django.db import models

class Book(models.Model):
    WILLIAM_SHAKESPEARE = 'WS'
    MIGUEL_DE_CERVANTES = 'MC'
    AGATHA_CHRISTIE = 'AC'

    AUTHOR_CHOICES = [
        (WILLIAM_SHAKESPEARE, 'William Shakespeare'),
        (MIGUEL_DE_CERVANTES, 'Miguel de Cervantes'),
        (AGATHA_CHRISTIE, 'Agatha Christie'),
    ]

    title = models.CharField(max_length=512)
    author = models.CharField(max_length=512, choices=AUTHOR_CHOICES)

Nothing changed under the hood, so there’s no need to make additional migrations.

Additionally, Django supports grouping choices into groups. This is made using sequence data as shown in the following example (authors are grouped by the century they were born):

from django.db import models


class Book(models.Model):
   AUTHOR_CHOICES = [
       ('16th century', (
           ('WS', 'William Shakespeare'),
           ('MC', 'Miguel de Cervantes'),
       )
        ),
       ('19th century', (
           ('AC', 'Agatha Christie'),
       )
        ),
   ]

   title = models.CharField(max_length=512)
   author = models.CharField(max_length=512, choices=AUTHOR_CHOICES)

Accordingly, we need to make our migrations again and migrate changes to the database.
The Django Admin interface changed for adding Authors while creating a new book object.

Django model choices in Admin panel with grouping.

It is now grouped by centuries.

Django Field Choices using Choices class

The Choices class represents the enumeration type. Enumeration is a data structure that binds a name to its constant value. Model subclasses this class, and it defines it similarly to the sequence. The name represents the label, and its value represents the value saved to the database.

Let’s implement the Authors example using the Choices class.

from django.db import models

class Book(models.Model):
   class Author(models.TextChoices):
       WILLIAM_SHAKESPEARE = 'WS'
       MIGUEL_DE_CERVANTES = 'MC'
       AGATHA_CHRISTIE = 'AC'

   title = models.CharField(max_length=512)
   author = models.CharField(max_length=512, choices=Author.choices)

This example makes the same changes to the Django Admin interface as using sequence. Also, it’s important to notice that Django translates the variable’s name into a human-readable format. So, for example, WILLIAM_SHAKESPEARE is transformed into William Shakespeare.

While creating objects using Python console, you can now use Author class labels to prevent typo errors.

For example:

>>> Book(title="Don Quixote", author=Book.Author.MIGUEL_DE_CERVANTES).save()

Django, by default, provides two choices classes:

  • TextChoices
  • IntegerChoices
NOTE - If integer of string choices are not adequate for the type of data you are using, you can subclass Choices class and concrete data type.

Finally, let’s dive into choices validation.

Choices Validation

We’ve mentioned that no matter the choices we defined, we can still use the save() method to add data to columns with defined choices that don’t necessarily belong in choices. That is true. We can still create books with authors that are not specified in the sequence or enumeration type because the database doesn’t create any constraints to the defined column.

But what if we need to create a constraint to the column and allow it to add only the data specified in the choices field?
We can use the full_clean() method on the object and validate its data. If data is not accurate, ValidationError is raised.

We can see this in the following example:

>>> book = Book(title="Hamlet", author="William Shakespeare")
>>> book.full_clean() 
django.core.exceptions.ValidationError: {'author': ["Value 'William Shakespeare' is not a valid choice."]}

This is not a valid choice because we use a label instead of value for our author column, right usage would look like this:

>>> book = Book(title="Hamlet", author='WS')
>>> book.full_clean()
>>> book.save()

The full_clean method is explained in Django documentation.

Conclusion

Model choices are very helpful and more accessible to implement than many-to-one relationships. But you need to remember that they are not very scalable, and if there is a slight chance that choices could be dynamically added or removed, they are not the right path to choose. I hope this article helped you!