How to properly use the "choices" field option in Django

PythonPython 3.xDjangoDjango ModelsEnums

Python Problem Overview


I'm reading the tutorial here: https://docs.djangoproject.com/en/1.5/ref/models/fields/#choices and i'm trying to create a box where the user can select the month he was born in. What I tried was

 MONTH_CHOICES = (
    (JANUARY, "January"),
    (FEBRUARY, "February"),
    (MARCH, "March"),
    ....
    (DECEMBER, "December"),
)

month = CharField(max_length=9,
                  choices=MONTHS_CHOICES,
                  default=JANUARY)

Is this correct? I see that in the tutorial I was reading, they for some reason created variables first, like so

FRESHMAN = 'FR'
SOPHOMORE = 'SO'
JUNIOR = 'JR'
SENIOR = 'SR'

Why did they create those variables? Also, the MONTHS_CHOICES is in a model called People, so would the code I provided create a "Months Choices) column in the database called called "People" and would it say what month the user was born in after he clicks on of the months and submits the form?

Python Solutions


Solution 1 - Python

I think no one actually has answered to the first question:

> Why did they create those variables?

Those variables aren't strictly necessary. It's true. You can perfectly do something like this:

MONTH_CHOICES = (
    ("JANUARY", "January"),
    ("FEBRUARY", "February"),
    ("MARCH", "March"),
    # ....
    ("DECEMBER", "December"),
)

month = models.CharField(max_length=9,
                  choices=MONTH_CHOICES,
                  default="JANUARY")

Why using variables is better? Error prevention and logic separation.

JAN = "JANUARY"
FEB = "FEBRUARY"
MAR = "MAR"
# (...)

MONTH_CHOICES = (
    (JAN, "January"),
    (FEB, "February"),
    (MAR, "March"),
    # ....
    (DEC, "December"),
)

Now, imagine you have a view where you create a new Model instance. Instead of doing this:

new_instance = MyModel(month='JANUARY')

You'll do this:

new_instance = MyModel(month=MyModel.JAN)

In the first option you are hardcoding the value. If there is a set of values you can input, you should limit those options when coding. Also, if you eventually need to change the code at the Model layer, now you don't need to make any change in the Views layer.

Solution 2 - Python

For Django3.0+, use models.TextChoices (see docs-v3.0 for enumeration types)

from django.db import models

class MyModel(models.Model):
    class Month(models.TextChoices):
        JAN = '1', "JANUARY"
        FEB = '2', "FEBRUARY"
        MAR = '3', "MAR"
        # (...)

    month = models.CharField(
        max_length=2,
        choices=Month.choices,
        default=Month.JAN
    )

Usage::

>>> obj = MyModel.objects.create(month='1')
>>> assert obj.month == obj.Month.JAN == '1'
>>> assert MyModel.Month(obj.month) is obj.Month.JAN
>>> assert MyModel.Month(obj.month).value is '1'
>>> assert MyModel.Month(obj.month).label == 'JANUARY'
>>> assert MyModel.Month(obj.month).name == 'JAN'
>>> assert MyModel.objects.filter(month=MyModel.Month.JAN).count() >= 1

>>> obj2 = MyModel(month=MyModel.Month.FEB)
>>> assert obj2.get_month_display() == obj2.Month(obj2.month).label

Let's say we known the label is 'JANUARY', how to get the name 'JAN' and the value '1'?

label = "JANUARY"
name = {i.label: i.name for i in MyModel.Month}[label]
print(repr(name))  # 'JAN'
value = {i.label: i.value for i in MyModel.Month}[label]
print(repr(value))  # '1'

Personally, I would rather use models.IntegerChoices

class MyModel(models.Model):
    class Month(models.IntegerChoices):
        JAN = 1, "JANUARY"
        FEB = 2, "FEBRUARY"
        MAR = 3, "MAR"
        # (...)

    month = models.PositiveSmallIntegerField(
        choices=Month.choices,
        default=Month.JAN
    )

Solution 3 - Python

According to the documentation:

> Field.choices > > An iterable (e.g., a list or tuple) consisting itself of > iterables of exactly two items (e.g. [(A, B), (A, B) ...]) to use as > choices for this field. If this is given, the default form widget will > be a select box with these choices instead of the standard text field. > > The first element in each tuple is the actual value to be stored, and > the second element is the human-readable name.

So, your code is correct, except that you should either define variables JANUARY, FEBRUARY etc. or use calendar module to define MONTH_CHOICES:

import calendar
...

class MyModel(models.Model):
    ...

    MONTH_CHOICES = [(str(i), calendar.month_name[i]) for i in range(1,13)]

    month = models.CharField(max_length=9, choices=MONTH_CHOICES, default='1')

Solution 4 - Python

The cleanest solution is to use the django-model-utils library:

from model_utils import Choices

class Article(models.Model):
    STATUS = Choices('draft', 'published')
    status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)

https://django-model-utils.readthedocs.io/en/latest/utilities.html#choices

Solution 5 - Python

I would suggest to use django-model-utils instead of Django built-in solution. The main advantage of this solution is the lack of string declaration duplication. All choice items are declared exactly once. Also this is the easiest way for declaring choices using 3 values and storing database value different than usage in source code.

from django.utils.translation import ugettext_lazy as _
from model_utils import Choices

class MyModel(models.Model):
   MONTH = Choices(
       ('JAN', _('January')),
       ('FEB', _('February')),
       ('MAR', _('March')),
   )
   # [..]
   month = models.CharField(
       max_length=3,
       choices=MONTH,
       default=MONTH.JAN,
   )

And with usage IntegerField instead:

from django.utils.translation import ugettext_lazy as _
from model_utils import Choices

class MyModel(models.Model):
   MONTH = Choices(
       (1, 'JAN', _('January')),
       (2, 'FEB', _('February')),
       (3, 'MAR', _('March')),
   )
   # [..]
   month = models.PositiveSmallIntegerField(
       choices=MONTH,
       default=MONTH.JAN,
   )
  • This method has one small disadvantage: 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).

Solution 6 - Python

You can't have bare words in the code, that's the reason why they created variables (your code will fail with NameError).

The code you provided would create a database table named month (plus whatever prefix django adds to that), because that's the name of the CharField.

But there are better ways to create the particular choices you want. See a previous Stack Overflow question.

import calendar
tuple((m, m) for m in calendar.month_name[1:])

Solution 7 - Python

$ pip install django-better-choices

For those who are interested, I have created django-better-choices library, that provides a nice interface to work with Django choices for Python 3.7+. It supports custom parameters, lots of useful features and is very IDE friendly.

You can define your choices as a class:

from django_better_choices import Choices


class PAGE_STATUS(Choices):
    CREATED = 'Created'
    PENDING = Choices.Value('Pending', help_text='This set status to pending')
    ON_HOLD = Choices.Value('On Hold', value='custom_on_hold')

    VALID = Choices.Subset('CREATED', 'ON_HOLD')

    class INTERNAL_STATUS(Choices):
        REVIEW = 'On Review'

    @classmethod
    def get_help_text(cls):
        return tuple(
            value.help_text
            for value in cls.values()
            if hasattr(value, 'help_text')
        )

Then do the following operations and much much more:

print( PAGE_STATUS.CREATED )                # 'created'
print( PAGE_STATUS.ON_HOLD )                # 'custom_on_hold'
print( PAGE_STATUS.PENDING.display )        # 'Pending'
print( PAGE_STATUS.PENDING.help_text )      # 'This set status to pending'

'custom_on_hold' in PAGE_STATUS.VALID       # True
PAGE_STATUS.CREATED in PAGE_STATUS.VALID    # True

PAGE_STATUS.extract('CREATED', 'ON_HOLD')   # ~= PAGE_STATUS.VALID

for value, display in PAGE_STATUS:
    print( value, display )

PAGE_STATUS.get_help_text()
PAGE_STATUS.VALID.get_help_text()

And of course, it is fully supported by Django and Django Migrations:

class Page(models.Model):
    status = models.CharField(choices=PAGE_STATUS, default=PAGE_STATUS.CREATED)

Full documentation here: https://pypi.org/project/django-better-choices/

Solution 8 - Python

Mar, 2022 Update:

The simplest, easiest, best and new way is using "models.TextChoices" which is built-in which means "You don't need to install any packages".

"models.py":

from django.db import models

class MyModel(models.Model):

    class Months(models.TextChoices):
        JANUARY = 'JAN', 'January'
        FEBRUARY = 'FEB', 'February'
        MARCH = 'MAR', 'March'
        APRIL = 'APR', 'April'
        MAY = 'MAY', 'May'

    month = models.CharField(
        max_length=3,
        choices=Months.choices,
        default=Months.APRIL 
    )

    class YearInSchool(models.TextChoices):
        FRESHMAN = 'FR', 'Freshman'
        SOPHOMORE = 'SO', 'Sophomore'
        JUNIOR = 'JR', 'Junior'
        SENIOR = 'SR', 'Senior'
        GRADUATE = 'GR', 'Graduate'

    year_in_school = models.CharField(
        max_length=2,
        choices=YearInSchool.choices,
        default=YearInSchool.SOPHOMORE,
    )

I also rewrote the code above in the old way which is also built-in.

"models.py":

from django.db import models

class MyModel(models.Model):

    JANUARY = 'JAN'
    FEBRUARY = 'FEB'
    MARCH = 'MAR'
    APRIL = 'APR'
    MAY = 'MAY'

    MANTHS = [
        (JANUARY, 'January'),
        (FEBRUARY, 'February'),
        (MARCH, 'March'),
        (APRIL, 'April'),
        (MAY, 'May'),
    ]

    month = models.CharField(
        max_length=3,
        choices=MANTHS,
        default=APRIL # or "default=MANTHS[4]"
    )

    FRESHMAN = 'FR'
    SOPHOMORE = 'SO'
    JUNIOR = 'JR'
    SENIOR = 'SR'
    GRADUATE = 'GR'

    YEAR_IN_SCHOOL_CHOICES = [
        (FRESHMAN, 'Freshman'),
        (SOPHOMORE, 'Sophomore'),
        (JUNIOR, 'Junior'),
        (SENIOR, 'Senior'),
        (GRADUATE, 'Graduate'),
    ]

    year_in_school = models.CharField(
        max_length=2,
        choices=YEAR_IN_SCHOOL_CHOICES,
        default=SOPHOMORE # or "default=YEAR_IN_SCHOOL_CHOICES[1]"
    )

As you can see, the new way is much simpler than the old way.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSilentDevView Question on Stackoverflow
Solution 1 - PythonJCJSView Answer on Stackoverflow
Solution 2 - PythonWaket ZhengView Answer on Stackoverflow
Solution 3 - PythonalecxeView Answer on Stackoverflow
Solution 4 - PythonBabken VardanyanView Answer on Stackoverflow
Solution 5 - PythondtarView Answer on Stackoverflow
Solution 6 - PythonPaulo AlmeidaView Answer on Stackoverflow
Solution 7 - PythonVisioNView Answer on Stackoverflow
Solution 8 - PythonKai - Kazuya ItoView Answer on Stackoverflow