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.


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.HIGHUsing 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.


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 0Here’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:
- Use Django Built-In Login System
- Build a To-Do List API in Django Rest Framework
- Create a Notes Taking app in Django
- Retrieve GET Parameters from Django Requests

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.