How to use enums as a choice field in django model

PythonDjangoPython 3.xDjango ModelsEnums

Python Problem Overview


I have a model class of which I want two fields to be a choice fields, so to populate those choices I am using an enum as listed below

#models.py
class Transaction(models.Model):
    trasaction_status = models.CharField(max_length=255, choices=TransactionStatus.choices())
    transaction_type = models.CharField(max_length=255, choices=TransactionType.choices())

#enums.py
class TransactionType(Enum):

    IN = "IN",
    OUT = "OUT"

    @classmethod
    def choices(cls):
        print(tuple((i.name, i.value) for i in cls))
        return tuple((i.name, i.value) for i in cls)

class TransactionStatus(Enum):

    INITIATED = "INITIATED",
    PENDING = "PENDING",
    COMPLETED = "COMPLETED",
    FAILED = "FAILED"
    ERROR = "ERROR"

    @classmethod
    def choices(cls):
        print(tuple((i.name, i.value) for i in cls))
        return tuple((i.name, i.value) for i in cls)

However, when I am trying to access this model through admin I am getting the following error :

Django Version:	1.11
Exception Type:	ValueError
Exception Value:	
too many values to unpack (expected 2)

I followed two articles that described how to use enums:

Python Solutions


Solution 1 - Python

Django 3.0 has built-in support for Enums

Example:

> python > from django.utils.translation import gettext_lazy as _ > > class Student(models.Model): > > 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.FRESHMAN, > ) > > > These work similar to enum from Python’s standard library, but with some modifications: > > - Enum member values are a tuple of arguments to use when constructing the concrete data type. Django supports adding an extra string value to the end of this tuple to be used as the human-readable name, or label. The label can be a lazy translatable string. Thus, in most cases, the member value will be a (value, label) two-tuple. If a tuple is not provided, or the last item is not a (lazy) string, the label is automatically generated from the member name. > - A .label property is added on values, to return the human-readable name. > A number of custom properties are added to the enumeration classes – .choices, .labels, .values, and .names – to make it easier to access lists of those separate parts of the enumeration. Use .choices as a suitable value to pass to choices in a field definition. > - The use of enum.unique() is enforced to ensure that values cannot be defined multiple times. This is unlikely to be expected in choices for a field.

For more info, check the documentation

Note:

As @Danielle Madeley pointed out, if you try to access the year_in_school attribute directly Django still returns the raw string instead of the Enum object:

>>> student.year_in_school
'FR'

What I usually do is to create a helper method that returns the Enum object:

class Student(models.Model):
    ...

    def get_year_in_school() -> YearInSchool:
        # Get value from choices enum
        return self.YearInSchool[self.year_in_school]

Solution 2 - Python

For Django 2.x and lower:

You define an Enum by setting the various options as documented here:

class TransactionStatus(Enum):

    INITIATED = "INITIATED"
    PENDING = "PENDING"
    COMPLETED = "COMPLETED"
    FAILED = "FAILED"
    ERROR = "ERROR"

Note there are no commas! This allows you later in your code to refer to TransactionStatus.ERROR or TransactionStatus.PENDING.

The rest of your code is correct. You get the choices by creating tuples of option.name, option.value.

UPDATE: For Django 3.x and higher, use the built-in types TextChoices, IntegerChoices and Choices as described here. That way you don't have to construct the choices tuple yourself.

Solution 3 - Python

Problem in your code is that INITIATED = "INITIATED", a comma after INITIATED option and other options. when we add comma after any string it will become a tuple. See an example below

s = 'my str'
print(type(s))
# output: str

s = 'my str',
print(type(s))
# output: tuple

#models.py

class Transaction(models.Model):
    trasaction_status = models.CharField(max_length=255, choices=TransactionStatus.choices())
    transaction_type = models.CharField(max_length=255, choices=TransactionType.choices())

#enums.py

class TransactionType(Enum):

    IN = "IN"
    OUT = "OUT"

    @classmethod
    def choices(cls):
        print(tuple((i.name, i.value) for i in cls))
        return tuple((i.name, i.value) for i in cls)

class TransactionStatus(Enum):

    INITIATED = "INITIATED"
    PENDING = "PENDING"
    COMPLETED = "COMPLETED"
    FAILED = "FAILED"
    ERROR = "ERROR"

    @classmethod
    def choices(cls):
        print(tuple((i.name, i.value) for i in cls))
        return tuple((i.name, i.value) for i in cls)

For django > 3.0 https://docs.djangoproject.com/en/4.0/ref/models/fields/#field-choices-enum-types

Solution 4 - Python

If you are receiving this error:

> 'choices' must be an iterable containing (actual value, human readable name) tuples

And are using Django3, then you are probably running into the same issue I did: The "Enums" have to be embedded in the model where you are trying to use them and ca not be declared outside of the model. For example, this will not work:

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

class Student(models.Model):
   year_in_school = models.CharField(
        max_length=2,
        choices=YearInSchool.choices,
        default=YearInSchool.FRESHMAN,
    )

Where as this example from the docs will:

class Student(models.Model):

    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.FRESHMAN,
    )

Solution 5 - Python

By the way Djanog also supports the Python 3's auto() as the Enum value. You can use the following helperclass to make your life easier.

from django.db.models.enums import TextChoices

class AutoEnumChoices(TextChoices):
    def _generate_next_value_(name, start, count, last_values):  # @NoSelf
        return name.lower()
    
    @property
    def choices(cls):  # @NoSelf
        empty = [(None, cls.__empty__)] if hasattr(cls, '__empty__') else []
        return empty + [(member.value, member.label) for member in cls]

Then use it in your choices definition:

class TransferBasicStatus(AutoEnumChoices):
    NONE = auto()
    WAITING = auto()
    PENDING = auto()
    PROGRESS = auto()
    SUCCESS = auto()
    DECLINED = auto()
    ENDED =  'ended', _('Ended - The transfer has ended with mixed states')

Solution 6 - Python

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.FRESHMAN,
    )

For above Django 3.0, You can use the above example.

For Integer Choices you can use the below code.

class Suit(models.IntegerChoices):
        DIAMOND = 1
        SPADE = 2
        HEART = 3
        CLUB = 4

    suit = models.IntegerField(choices=Suit.choices)

Solution 7 - Python

An example from my project:

import enum

from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _


class NotificationTemplate(models.Model):
class Meta:
    verbose_name = _('notification template')
    verbose_name_plural = _('notification templates')

@enum.unique
class Name(str, enum.Enum):
    ONBOARDING = 'onboarding'
    TG_ERROR = 'tg_error'
    FB_ERROR = 'fb_error'

    @classmethod
    def choices(cls):
        return [(item.value, item.name) for item in cls]

@enum.unique
class Type(int, enum.Enum):
    PUSH = 1
    EMAIL = 2
    TELEGRAM = 3
    VK = 4
    OTHER = 5

    @classmethod
    def choices(cls):
        return [(item.value, item.name) for item in cls]

name = models.CharField(_('notification name'), max_length=64, unique=True, choices=Name.choices(), default=Name.ONBOARDING)
template_type = ArrayField(models.PositiveSmallIntegerField(_('type'), choices=Type.choices()))
max_count = models.PositiveSmallIntegerField(default=1)

def __str__(self):
    return self.Name(self.name).name

Solution 8 - Python

According to your reference from https://hackernoon.com/using-enum-as-model-field-choice-in-django-92d8b97aaa63. The choices should be list of tuple, while yours will return a tuple of tuple. More over i is different from i.name. Try:

#enums.py
class TransactionType(Enum):
    IN = "IN"
    OUT = "OUT"

    @classmethod
    def choices(cls):
        return [(i, i.value) for i in cls]

Solution 9 - Python

You can try doing something like this based on examples from docs.:

from enum import Enum

class BaseEnum(Enum):
	def __new__(cls, *args):
		obj = object.__new__(cls)
		obj._value_ = args[0]
		obj.display_name = args[1]
		return obj

	@classmethod
	def model_choices(cls):
		return [(cls.__members__[member].value, cls.__members__[member].display_name)
			for member in cls.__members__.keys()]

which would result in:

>>> class TransactionType(BaseEnum):
...     IN = ('in', 'In')
...     OUT = ('out', 'Out')
...
>>> TransactionType.IN.value
'in'
>>> TransactionType.IN.display_name
'In'
>>> TransactionType.model_choices()
[('in', 'In'), ('out', 'Out')]

which could be used as an argument for a field's choices.

Solution 10 - Python

It is also possible to write:

class Transaction(models.Model):
    class TransactionStatus(Enum):
        initiated = ('in', 'Initiated')
        pending = ('pe', 'Pending')
        completed = ('co', 'Completed')
        failed = ('fa', 'Failed')
        error = ('er', 'Error')
        
        @classmethod
        def get_value(cls, member):
            return cls[member].value[0]
    
    class TransactionType(Enum):
        _in = ('in', 'In')
        out = ('ou', 'Out')
   
        @classmethod
        def get_value(cls, member):
            return cls[member].value[0]

    trasaction_status = models.CharField(max_length=2, choices=[x.value for x in TransactionStatus])
    transaction_type = models.CharField(max_length=2, choices=[x.value for x in TransactionType])

With get_value you can write for example:

Transaction.objects.filter(status=Transaction.TransactionStatus.get_value('initialited'))

Solution 11 - Python

@paras you have to change your model @classmethod def choices(cls): print(tuple((i.value, i.name) for i in cls)) return tuple((i.value, i.name) for i in cls)

it worked for me.

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
QuestionParasView Question on Stackoverflow
Solution 1 - PythonCesar CanassaView Answer on Stackoverflow
Solution 2 - PythondirkgrotenView Answer on Stackoverflow
Solution 3 - Pythonanjaneyulubatta505View Answer on Stackoverflow
Solution 4 - PythonTheRightChoyceView Answer on Stackoverflow
Solution 5 - PythonKevin ParkerView Answer on Stackoverflow
Solution 6 - PythonHardik GajjarView Answer on Stackoverflow
Solution 7 - Pythonrail.khayrullinView Answer on Stackoverflow
Solution 8 - PythonagmadonView Answer on Stackoverflow
Solution 9 - PythonUllauriView Answer on Stackoverflow
Solution 10 - PythonRedWheelbarrowView Answer on Stackoverflow
Solution 11 - PythonKaran GothwalView Answer on Stackoverflow