Django models: Declaring a list of available choices in the right way

Django models: pros and cons of each solution

One of the examples of such problem is choosing the right way to declare a list of available choices passed to Django model fields (eg. CharField). I’ll try to compare different ways of declaring Django choices list, pointing out the advantages and drawbacks of each solution.

I’ll start this comparison from the easiest and [no requiring any external module] solutions and next, I’ll present third-party packages also solving this issue. For all examples, the same simple model Book with Foreign Key relation to Author is used:

from django.db import modelsclass Author(models.Model):
first_name = models.CharField(max_length=64)
last_name = models.CharField(max_length=64) def __str__(self):
return '{} {}'.format(self.first_name, self.last_name)
class Book(models.Model):
author = models.ForeignKey('books.Author', related_name='books', on_delete=models.CASCADE)
title = models.CharField(max_length=128)
status = models.CharField(
max_length=32,
choices=[], # some list of choices
) def __str__(self):
return '{}: {}'.format(self.author, self.title)

Django fields require to get an iterable (eg. list or tuple) as a parameter choices. Each element of this list should be a tuple representing:

(value, display_name) or (group_name, group_choices)

where value is the value stored in the database and display_name is a human-readable text describing this choice. For the second possible parameters structure, group_name is a name of grouping items and group_choices is an iterable object with tuples (value, display_name). For the sake of all examples, we will focus on the first choices declaration, but the same rule applies to the choices with the grouping element. 

Choices declaration improvement

In my opinion, whenever it’s possible integers should be used for storing information about some resource status. There is a big advantage of such a solution. After adding a database index to that field, search performance will be more efficient when using integer field instead of a char field.  Another advantage is the fact that we can manipulate ordering on such field. For example:
In the model there is a status field with the allowed choices:
New = 1, Draft = 2, Published = 3.
If we want to order those Django models instances by status from the new created to published, we can now easily add ordering on that field (ascending on integer field). 

According to this, whenever it’s possible we’ll use the following model:

class Book(models.Model):
author = models.ForeignKey('books.Author', related_name='books', on_delete=models.CASCADE)
title = models.CharField(max_length=128)
status = models.PositiveSmallIntegerField(
choices=[],
)

Built-in Django solutions

First approach

The first approach (the easiest one) for declaring such list is only to use python’s tuple:

class Book(models.Model):
STATUS = (
('available', _('Available to borrow')),
('borrowed', _('Borrowed by someone')),
('archived', _('Archived - not available anymore')),
) # […]
status = models.CharField(
max_length=32,
choices=STATUS,
default='available',
)

Or when we use integer field:

class Book(models.Model):
STATUS = (
(1, _('Available to borrow')),
(2, _('Borrowed by someone')),
(3, _('Archived - not available anymore')),
)
# [...]
status = models.PositiveSmallIntegerField(
choices=STATUS,
default=1,
)

In this approach, we are obligated to declare a tuple which contains another tuple (value, display_text).

Advantage:

  • no external package is required. 

Disadvantages:

  • there’s no elegant way to get value from STATUS tuple declaration, only by using list indexes - STATUS[0][0] to get available status,
  • in line 12 where their statement is: default='available', we’re enforced to use hardcoded string or “magic” number,
  • using the hardcoded string may lead to a situation, where values in the database will be different that declared as available choices,
  • no code completion or error highlighting in any IDE when we’ve used bad choice value. 


Second approach: Django Best Practice preferred way

That’s the second approach suggested by Django documentation and “Two scoops of Django 1.11”. Let’s see on the listing below:

class Book(models.Model):
AVAILABLE = 'available'
BORROWED = 'borrowed'
ARCHIVED = 'archived'
STATUS = [
(AVAILABLE, _('Available to borrow')),
(BORROWED, _('Borrowed by someone')),
(ARCHIVED, _('Archived - not available anymore')),
]
# […]
status = models.CharField(
max_length=32,
choices=STATUS,
default=AVAILABLE,
)

Or when using integer fields:

class Book(models.Model):
AVAILABLE = 1
BORROWED = 2
ARCHIVED = 3
STATUS = (
(AVAILABLE, _('Available to borrow')),
(BORROWED, _('Borrowed by someone')),
(ARCHIVED, _('Archived - not available anymore')),
)
# […]
status = models.PositiveSmallIntegerField(
choices=STATUS,
default=AVAILABLE,
)

It’s the same as the first solution, but all suitably-named constant for each choice value should also be defined in the model class.

Advantages: 

  • the main advantage is that the software developer now has access to available status using a variable, not a hardcoded string,
  • no external package is required. 

Disadvantages: 

  • produce more code lines, especially when there is plenty of allowed choices, each status has to be declared in two lines.

Third approach: Python 3 Enum class

Following the suggestion from “Two scoops of Django 1.11” I implemented also providing a list of allowed choices as an enum class:

class Book(models.Model):
class STATUS(Enum):
available = ('available', 'Available to borrow')
borrowed = ('borrowed', 'Borrowed by someone')
archived = ('archived', 'Archived - not available anymore') @classmethod
def get_value(cls, member):
return cls[member].value[0]
# [...]
status = models.CharField(
max_length=32,
choices=[x.value for x in STATUS],
default=STATUS.get_value('available'),
)

For storing choices in integer field: 

class Book(models.Model):
class STATUS(Enum):
available = (1, 'Available to borrow')
borrowed = (2, 'Borrowed by someone')
archived = (3, 'Archived - not available anymore') @classmethod
def get_value(cls, member):
return member.value[0]
# [...]
status = models.CharField(
max_length=32,
choices=[x.value for x in STATUS],
default=STATUS.get_value(STATUS.available), # OR STATUS.available[0]
)

To use solution based on Enum class we have to declare a class which inherits from Enum inside Book model. Each choice as a value should have a tuple with the value stored in the database and human-readable text. To get easy access to database value, we should implement some helper method like get_value().

Advantages:

  • the big advantage of using enum class is the fact that members of the enum class have to be unique, so all mistakes with duplicated choice names will be automatically rejected on application runtime,
  • it’s simple to use other value to store choice in the database and the other in source code, 
  • If we work on Python3 version, there is no external package required, if we stuck on Python2 there is a module named enum.

Disadvantages:

  • the main drawback of this solution is that there is no simple way to get some choice value. In the above code the existence of additional method get_value is practically essential to enable getting choice value and in fact is not the most readable construction,
  • eventually, skipping the implementation of additional method like get_value will lead to using tuple indexing (STATUS.available[0]) but as in previous approaches, it’s not the clean code.
  • Enum class was designed to  use it for storing constants names for integers like in listing:

class Color(Enum)
RED = 1
BLUE = 2

so for some software developers (including me), it’s not the best solution to use this class for other values than integers.

Third-party packages

I found a few different approaches for declaring a list of available choices as an external python package. 

Django-model-utils 

Based on the Django Packages comparison, this module is the most popular and have the most GitHub stars, but this package is not designed only for declaring available choices and has a lot of different useful tools. Using these packages, the implementation looks like:

from model_utils import Choicesclass Book(models.Model):
STATUS = Choices(
('available', _('Available to borrow')),
('borrowed', _('Borrowed by someone')),
('archived', _('Archived - not available anymore')),
)
# [..]
status = models.CharField(
max_length=32,
choices=STATUS,
default=STATUS.available,
)

And with usage IntegerField instead:

from model_utils import Choicesclass Book(models.Model):
STATUS = Choices(
(1, 'available', _('Available to borrow')),
(2, 'borrowed', _('Borrowed by someone')),
(3, 'archived', _('Archived - not available anymore')),
)
# [...]
status = models.PositiveSmallIntegerField(
choices=STATUS,
default=STATUS.available,
)

Based on django-model-utils we should create an instance of object Choices, as an argument passing tuples of allowed choices. Each tuple consists of (database_value, name, display_text), we can omit to provide name parameter then it will be the same value assigned as to database_value

Advantages:

  • the main advantage of this solution is the lack of string declaration duplication. All choice items are declared exactly once,
  • an opportunity to access the choice value by STATUS.available,
  • easy way for declaring choices using 3 values and storing database value different than usage in source code.

Disadvantages: 

  • in any IDE (eg. PyCharm) there will be no code completion for available choices (it’s because those values aren’t standard members of Choices class). So, you’ll have to directly write STATUS.available and for me, it’s a bit annoying but it’s rather low importance weakness,
  • requires installing external package,

Django-utils2 and django-choices

Those two packages provide a very similar approach to defining available choices. We have to create a class which has to inherit from:

  • Choices when we use django-utils2
  • DjangoChoices when we use django-choices.

Choices are attributes of this class, each of them has to be an instance of:

  • Choice class for using django-utils2,
  • ChoiceItem for django-choices.

django-utils2 has also some other modules and helpers methods, django-choices is designed directly for easing the declaration list of available choices. 

Code snippets based on package django-utils2:

from django_utils.choices import Choice, Choicesclass Book(models.Model):
class STATUS(Choices):
available = Choice('available', _('Available to borrow'))
borrowed = Choice('borrowed', _('Borrowed by someone'))
archived = Choice('archived', _('Archived - not available anymore')) # [...]
status = models.CharField(
max_length=32,
choices=STATUS.choices,
default=STATUS.available,
)

Or when based on Integer for storing statuses:

class Book(models.Model):
class STATUS(Choices):
available = Choice(1, _('Available to borrow')) # or simpler: `Choice(label= _('Available to borrow')) `
borrowed = Choice(2 _('Borrowed by someone'))
archived = Choice(3, _('Archived - not available anymore')) # [...]
status = models.PositiveSmallIntegerField(
choices=STATUS.choices,
default=STATUS.available,
)

All choice items should be declared in inherited from Choices class. Each status is an attribute of this class with the instance of Choice as a value. The Choice object in the constructor takes two parameters: value and label, both are optional. When not passing any arguments, the choice attribute will have auto-incremented integer value starting from 1. 

Advantages:

  • similarly to the django-model-utils this solution gives us the possibility to easy access to choice item values STATUS.available
  • It’s also easy to represent choice items as 3 elements - database value, name and display text. 

Disadvantages:

  • comparing to django-model-utils if we want to declare a choice value to be a string and has the same as STATUS field member name we have to repeat the same name twice,
  • requires installing additional package. 

Django models - is there the only right way?

As we’ve seen, there are many ways to declare a list of available Django models choices. There is no the only one right way, it all depends on software developer preferences. Using Django built-in solution according to Django documentation might be sufficient and the best option for many Django developers. But if you don’t have any doubts about using too many external packages, I recommend using django-model-utils or django-utils2.

Navigate the changing IT landscape

Some highlighted content that we want to draw attention to to link to our other resources. It usually contains a link .