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 .venv must 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.


Never miss a story on Django.wiki

Subscribe for fresh tutorials, snippets, and updates.

By subscribing you agree to our Privacy Policy.