Choices And Enums For Fields
Add constrained values with choices and Python enums. Learn display values, database storage, migrations impact, and refactoring tips.
1. Introduction
Choices let you restrict a field to a fixed set of allowed values. Instead of accepting any string, a status field might only accept 'draft', 'published', or 'archived'. Django enforces this in forms and the admin, and displays a human-readable label instead of the raw stored value.
In modern Django, the recommended way to define choices is with Python's built-in TextChoices or IntegerChoices enums. This guide covers both the old tuple approach and the modern enum approach, and explains when to use each.
- You should already have a working model in
pages/models.py. - Your
.venvmust be active and Django 5.2 installed.
2. The tuple approach
The traditional way to define choices is a list of tuples. The first value is what gets stored in the database. The second is the human-readable label shown in forms and the admin.
class Article(models.Model):
STATUS_DRAFT = 'draft'
STATUS_PUBLISHED = 'published'
STATUS_ARCHIVED = 'archived'
STATUS_CHOICES = [
(STATUS_DRAFT, 'Draft'),
(STATUS_PUBLISHED, 'Published'),
(STATUS_ARCHIVED, 'Archived'),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default=STATUS_DRAFT,
)
This works fine but has a problem — the constants and choices list are scattered inside the model class and easy to get out of sync. The modern enum approach solves this.
3. TextChoices — the modern approach
Django 3.0 introduced TextChoices and IntegerChoices. They are Python enums that generate the choices list automatically and keep everything in one place.
from django.db import models
class Article(models.Model):
class Status(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
ARCHIVED = 'archived', 'Archived'
status = models.CharField(
max_length=20,
choices=Status,
default=Status.DRAFT,
)
def __str__(self):
return self.title
Each enum member has two values: the database value on the left and the human-readable label on the right. Django reads both automatically — no separate choices list needed.
You can access the label of any choice using .label:
Article.Status.DRAFT.label # 'Draft'
Article.Status.DRAFT.value # 'draft'
4. IntegerChoices
Use IntegerChoices when you want to store integers instead of strings. This is common for priority levels, ratings, or ordered states.
class Task(models.Model):
class Priority(models.IntegerChoices):
LOW = 1, 'Low'
MEDIUM = 2, 'Medium'
HIGH = 3, 'High'
CRITICAL = 4, 'Critical'
priority = models.IntegerField(
choices=Priority,
default=Priority.MEDIUM,
)
def __str__(self):
return f'{self.title} ({self.get_priority_display()})'
Notice get_priority_display() in the __str__ method. Django automatically generates a get_FIELDNAME_display() method for every field that has choices. It returns the human-readable label for the current value.
5. get_FIELDNAME_display()
This is one of the most useful automatic methods Django provides. For any field with choices, Django creates a method that returns the human-readable label:
article = Article.objects.get(id=1)
article.status # 'draft' (raw stored value)
article.get_status_display() # 'Draft' (human-readable label)
Use this in templates too:
<span>Status: </span>
6. Filtering by choices
Use the enum member directly in queries instead of hardcoding the string value. This prevents typos and makes refactoring easier:
# Good — uses the enum member
published = Article.objects.filter(status=Article.Status.PUBLISHED)
# Avoid — hardcoded string, breaks if you rename the value
published = Article.objects.filter(status='published')
7. Choices and migrations
Adding or changing choices does not change the database column — choices are a Python-level constraint only. However, Django still generates a migration when you change choices because the migration file records the choices for documentation purposes.
This means changing a label — for example from 'Published' to 'Live' — generates a migration even though nothing in the database changes. The migration is harmless but worth knowing about so you are not surprised when you see it.
8. Putting it together
Here is a complete model using TextChoices as an inner class — the pattern used throughout this tutorial series:
# pages/models.py
from django.db import models
class Article(models.Model):
class Status(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
ARCHIVED = 'archived', 'Archived'
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=220, unique=True)
content = models.TextField()
status = models.CharField(
max_length=20,
choices=Status,
default=Status.DRAFT,
)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
def is_published(self):
return self.status == self.Status.PUBLISHED
9. Next steps
You now know how to use choices and enums to restrict field values cleanly. The next step covers primary keys — how Django assigns them by default, when to change to BigAutoField, and when to use a UUID instead.