Django Model Choices with Enums

I was working on a Django project for a US-based e-commerce platform where I needed to handle order statuses, payment methods, and shipping options. The challenge was managing these fixed choices in a clean, maintainable way.

In this article, I’ll show you how to implement Enums as Django model choices using real-world examples. You’ll learn multiple approaches and best practices that I’ve developed over my years of Django development.

Let’s get in!

Methods to Use Django Model Choices with Enums

Before Django 3.0, developers typically used tuples to define model choices. While this worked, it had several limitations that I encountered in production applications.

Traditional tuple choices are prone to typos and make refactoring difficult. When you need to change a choice value, you have to hunt through your entire codebase.

Enums solve these problems by providing a centralized, type-safe way to define choices. They also offer better IDE support and make your code more readable.

Method 1 – Use TextChoices for String-Based Choices

Django 3.0 introduced TextChoices, which is perfect for string-based model fields. I use this approach when working with status fields or category selections.

Here’s a complete example using a US-based food delivery app:

# models.py
from django.db import models
from django.contrib.auth.models import User

class OrderStatus(models.TextChoices):
    PENDING = 'pending', 'Order Pending'
    CONFIRMED = 'confirmed', 'Order Confirmed'
    PREPARING = 'preparing', 'Being Prepared'
    OUT_FOR_DELIVERY = 'out_for_delivery', 'Out for Delivery'
    DELIVERED = 'delivered', 'Delivered'
    CANCELLED = 'cancelled', 'Cancelled'

class PaymentMethod(models.TextChoices):
    CREDIT_CARD = 'credit_card', 'Credit Card'
    DEBIT_CARD = 'debit_card', 'Debit Card'
    PAYPAL = 'paypal', 'PayPal'
    APPLE_PAY = 'apple_pay', 'Apple Pay'
    GOOGLE_PAY = 'google_pay', 'Google Pay'

class Order(models.Model):
    customer = models.ForeignKey(User, on_delete=models.CASCADE)
    status = models.CharField(
        max_length=20,
        choices=OrderStatus.choices,
        default=OrderStatus.PENDING
    )
    payment_method = models.CharField(
        max_length=15,
        choices=PaymentMethod.choices
    )
    total_amount = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"Order #{self.id} - {self.get_status_display()}"

    def can_cancel(self):
        return self.status in [OrderStatus.PENDING, OrderStatus.CONFIRMED]

    def mark_as_delivered(self):
        self.status = OrderStatus.DELIVERED
        self.save()

Now you can use these Enums in your views and forms:

# views.py
from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from .models import Order, OrderStatus

def update_order_status(request, order_id):
    order = get_object_or_404(Order, id=order_id)

    if request.method == 'POST':
        new_status = request.POST.get('status')

        # Type-safe comparison using Enum
        if new_status in OrderStatus.values:
            order.status = new_status
            order.save()

            return JsonResponse({
                'success': True,
                'status': order.get_status_display()
            })

    return JsonResponse({'success': False})

def order_dashboard(request):
    # Easy filtering using Enum values
    pending_orders = Order.objects.filter(status=OrderStatus.PENDING)
    delivered_orders = Order.objects.filter(status=OrderStatus.DELIVERED)

    context = {
        'pending_orders': pending_orders,
        'delivered_orders': delivered_orders,
        'status_choices': OrderStatus.choices,
    }

    return render(request, 'orders/dashboard.html', context)

You can see the output in the screenshot below.

django Enum
django enums

Method 2 – Use IntegerChoices for Numeric Values

Sometimes you need numeric choices, especially when working with priority levels or rating systems. Django’s IntegerChoices is perfect for this scenario.

Here’s an example using a customer support ticket system:

# models.py
from django.db import models

class TicketPriority(models.IntegerChoices):
    LOW = 1, 'Low Priority'
    MEDIUM = 2, 'Medium Priority'
    HIGH = 3, 'High Priority'
    URGENT = 4, 'Urgent'
    CRITICAL = 5, 'Critical'

class TicketCategory(models.TextChoices):
    TECHNICAL = 'technical', 'Technical Issue'
    BILLING = 'billing', 'Billing Inquiry'
    GENERAL = 'general', 'General Support'
    FEATURE_REQUEST = 'feature', 'Feature Request'
    BUG_REPORT = 'bug', 'Bug Report'

class SupportTicket(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    customer_email = models.EmailField()
    priority = models.IntegerField(
        choices=TicketPriority.choices,
        default=TicketPriority.MEDIUM
    )
    category = models.CharField(
        max_length=20,
        choices=TicketCategory.choices,
        default=TicketCategory.GENERAL
    )
    is_resolved = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-priority', '-created_at']

    def __str__(self):
        return f"{self.title} - {self.get_priority_display()}"

    @property
    def is_high_priority(self):
        return self.priority >= TicketPriority.HIGH

Using these Enums in your business logic becomes much cleaner:

# services.py
from django.core.mail import send_mail
from .models import SupportTicket, TicketPriority

def process_new_ticket(ticket):
    # Easy comparison using Enum values
    if ticket.priority >= TicketPriority.HIGH:
        notify_management(ticket)

    # Auto-assign based on priority
    if ticket.priority == TicketPriority.CRITICAL:
        assign_to_senior_team(ticket)

def notify_management(ticket):
    send_mail(
        subject=f'High Priority Ticket: {ticket.title}',
        message=f'A {ticket.get_priority_display()} ticket has been created.',
        from_email='support@company.com',
        recipient_list=['management@company.com']
    )

# views.py
from django.shortcuts import render
from django.db.models import Count
from .models import SupportTicket, TicketPriority, TicketCategory

def ticket_analytics(request):
    # Easy aggregation using Enum values
    priority_stats = SupportTicket.objects.values('priority').annotate(
        count=Count('id'),
        priority_label=models.Case(
            *[models.When(priority=choice[0], then=models.Value(choice[1])) 
              for choice in TicketPriority.choices]
        )
    )

    high_priority_count = SupportTicket.objects.filter(
        priority__gte=TicketPriority.HIGH
    ).count()

    context = {
        'priority_stats': priority_stats,
        'high_priority_count': high_priority_count,
    }

    return render(request, 'support/analytics.html', context)

You can see the output in the screenshot below.

django model Enum
django models enum

Method 3 – Custom Enum Classes for Complex Scenarios

For more complex scenarios, I create custom Enum classes that extend Django’s choice classes. This approach gives you maximum flexibility and type safety.

Here’s an example for a real estate application:

# enums.py
from django.db import models

class PropertyType(models.TextChoices):
    APARTMENT = 'apartment', 'Apartment'
    HOUSE = 'house', 'House'
    CONDO = 'condo', 'Condominium'
    TOWNHOUSE = 'townhouse', 'Townhouse'
    DUPLEX = 'duplex', 'Duplex'
    STUDIO = 'studio', 'Studio'

class PropertyStatus(models.TextChoices):
    AVAILABLE = 'available', 'Available'
    PENDING = 'pending', 'Pending'
    SOLD = 'sold', 'Sold'
    RENTED = 'rented', 'Rented'
    OFF_MARKET = 'off_market', 'Off Market'

class USState(models.TextChoices):
    ALABAMA = 'AL', 'Alabama'
    ALASKA = 'AK', 'Alaska'
    ARIZONA = 'AZ', 'Arizona'
    ARKANSAS = 'AR', 'Arkansas'
    CALIFORNIA = 'CA', 'California'
    COLORADO = 'CO', 'Colorado'
    CONNECTICUT = 'CT', 'Connecticut'
    DELAWARE = 'DE', 'Delaware'
    FLORIDA = 'FL', 'Florida'
    GEORGIA = 'GA', 'Georgia'
    # Add all other states as needed

# models.py
from django.db import models
from .enums import PropertyType, PropertyStatus, USState

class Property(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    property_type = models.CharField(
        max_length=15,
        choices=PropertyType.choices
    )
    status = models.CharField(
        max_length=15,
        choices=PropertyStatus.choices,
        default=PropertyStatus.AVAILABLE
    )
    price = models.DecimalField(max_digits=12, decimal_places=2)
    bedrooms = models.PositiveIntegerField()
    bathrooms = models.DecimalField(max_digits=3, decimal_places=1)
    square_feet = models.PositiveIntegerField()
    
    # Address fields
    street_address = models.CharField(max_length=200)
    city = models.CharField(max_length=100)
    state = models.CharField(
        max_length=2,
        choices=USState.choices
    )
    zip_code = models.CharField(max_length=10)
    
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"{self.title} - {self.get_property_type_display()}"
@property
    def is_available(self):
        return self.status == PropertyStatus.AVAILABLE

    def mark_as_sold(self):
        self.status = PropertyStatus.SOLD
        self.save()

    def get_full_address(self):
        return f"{self.street_address}, {self.city}, {self.get_state_display()} {self.zip_code}"
@property
def price_per_sqft(self):
    if self.square_feet > 0:
        return round(self.price / self.square_feet, 2)
    return 0

Here’s how to use these custom Enums effectively in your views and forms:

# views.py
from django.shortcuts import render
from django.db.models import Q, Count, Avg
from .models import Property
from .enums import PropertyType, PropertyStatus, USState

def property_search(request):
    properties = Property.objects.filter(status=PropertyStatus.AVAILABLE)
    
    # Filter by property type
    property_type = request.GET.get('type')
    if property_type and property_type in PropertyType.values:
        properties = properties.filter(property_type=property_type)
    
    # Filter by state
    state = request.GET.get('state')
    if state and state in USState.values:
        properties = properties.filter(state=state)
    
    context = {
        'properties': properties,
        'property_types': PropertyType.choices,
        'states': USState.choices,
        'selected_type': property_type,
        'selected_state': state,
    }
    
    return render(request, 'properties/search.html', context)

def market_analytics(request):
    # Analytics using Enum values
    type_distribution = Property.objects.values('property_type').annotate(
        count=Count('id'),
        avg_price=Avg('price')
    )
    
    # Convert to readable format
    formatted_data = []
    for item in type_distribution:
        property_type = item['property_type']
        readable_type = dict(PropertyType.choices)[property_type]
        formatted_data.append({
            'type': readable_type,
            'count': item['count'],
            'avg_price': item['avg_price']
        })
    
    context = {
        'type_distribution': formatted_data,
        'total_properties': Property.objects.count(),
        'available_count': Property.objects.filter(
            status=PropertyStatus.AVAILABLE
        ).count(),
    }
    
    return render(request, 'properties/analytics.html', context)

# forms.py
from django import forms
from .models import Property
from .enums import PropertyType, PropertyStatus, USState

class PropertyForm(forms.ModelForm):
    class Meta:
        model = Property
        fields = [
            'title', 'description', 'property_type', 'status',
            'price', 'bedrooms', 'bathrooms', 'square_feet',
            'street_address', 'city', 'state', 'zip_code'
        ]
        widgets = {
            'description': forms.Textarea(attrs={'rows': 4}),
            'property_type': forms.Select(choices=PropertyType.choices),
            'status': forms.Select(choices=PropertyStatus.choices),
            'state': forms.Select(choices=USState.choices),
        }

class PropertySearchForm(forms.Form):
    property_type = forms.ChoiceField(
        choices=[('', 'Any Type')] + PropertyType.choices,
        required=False
    )
    state = forms.ChoiceField(
        choices=[('', 'Any State')] + USState.choices,
        required=False
    )
    min_price = forms.DecimalField(required=False)
    max_price = forms.DecimalField(required=False)

Conclusion

After working with Django Enums for several years across different projects, I can confidently say they’ve transformed how I handle model choices. From managing order statuses in e-commerce platforms to categorizing support tickets, Enums provide the type safety and maintainability that traditional tuple choices simply can’t match.

The three methods I’ve shared – TextChoices for string values, IntegerChoices for numeric priorities, and custom Enum classes for complex scenarios – cover virtually every use case you’ll encounter. Each approach brings its advantages, and choosing the right one depends on your specific requirements.

You may like to read other Django-related articles:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.