How to use Grouped Model Choice Field in Django?

Hi Dev,
Are you looking for an efficient way to **use grouped model choice fields in Django**? This tutorial explains how to **organize choices within a Django select widget** and extract choice values from models. Here, you'll learn how to dynamically generate **grouped choices from Django models** and display them using the **optgroup HTML tag**.
Django’s Forms API provides two field types for working with multiple choices: **ChoiceField** and **ModelChoiceField**. Both render as **select input widgets**, but **ModelChoiceField** is tailored to handle **QuerySets and foreign key relationships** efficiently.
🔹 Example 1: Basic ChoiceField Implementation
A simple ChoiceField-based implementation:
forms.pyfrom django import forms class ChoicesForm(forms.Form): CHOICES = ( (1, 'Django'), (2, 'Python'), (3, 'PHP'), (4, 'JAVA'), (5, 'Laravel'), (6, 'Javascript'), ) language = forms.CharField(max_length=100, widget=forms.TextInput(attrs={'placeholder': 'Enter Language', 'class': 'form-control', })) category = forms.ChoiceField(choices=CHOICES, widget=forms.Select(attrs={'class': 'form-control', }))🔸 Preview

🔹 Example 2: Using Grouped Choice Field with Optgroup
To organize choices into **groups**, we use **optgroup** HTML tags:
forms.pyfrom django import forms class ChoicesForm(forms.Form): CHOICES = ( ('Gujarat', ( (1, 'Rajkot'), (2, 'Ahmedabad'), (3, 'Surat'), )), ('Maharashtra', ( (4, 'Mumbai'), (5, 'Pune'), )), ('Uttar Pradesh', ( (6, 'Lucknow'), (7, 'Agra'), )), ) state = forms.CharField(max_length=100, widget=forms.TextInput(attrs={'placeholder': 'Enter State Name', 'class': 'form-control', })) city = forms.ChoiceField(choices=CHOICES, widget=forms.Select(attrs={'class': 'form-control', }))🔸 Preview

🔹 Example 3: Grouped Model Choice Field Using Foreign Key
When using **ModelChoiceField**, Django doesn’t natively support grouping options. To **simulate grouped selections**, we introduce a **custom ModelChoiceField implementation**:
models.pyfrom django.db import models class Category(models.Model): name = models.CharField(max_length=30) parent = models.ForeignKey('Category', on_delete=models.CASCADE, null=True) def __str__(self): return self.name class Expense(models.Model): amount = models.DecimalField(max_digits=10, decimal_places=2) date = models.DateField() category = models.ForeignKey(Category, on_delete=models.CASCADE) def __str__(self): return self.amount
Next, create a module named **fields.py** for **grouping choices dynamically**:
fields.pyfrom functools import partial from itertools import groupby from operator import attrgetter from django.forms.models import ModelChoiceIterator, ModelChoiceField class GroupedModelChoiceIterator(ModelChoiceIterator): def __init__(self, field, groupby): self.groupby = groupby super().__init__(field) def __iter__(self): if self.field.empty_label is not None: yield ("", self.field.empty_label) queryset = self.queryset if not queryset._prefetch_related_lookups: queryset = queryset.iterator() for group, objs in groupby(queryset, self.groupby): yield (group, [self.choice(obj) for obj in objs]) class GroupedModelChoiceField(ModelChoiceField): def __init__(self, *args, choices_groupby, **kwargs): if isinstance(choices_groupby, str): choices_groupby = attrgetter(choices_groupby) elif not callable(choices_groupby): raise TypeError('choices_groupby must be a callable or a string') self.iterator = partial(GroupedModelChoiceIterator, groupby=choices_groupby) super().__init__(*args, **kwargs)
Use the custom grouped ModelChoiceField in your Django form:
forms.pyfrom django import forms from .fields import GroupedModelChoiceField from .models import Category, Expense class ExpenseForm(forms.ModelForm): category = GroupedModelChoiceField( queryset=Category.objects.exclude(parent=None), choices_groupby='parent' ) class Meta: model = Expense fields = ('amount', 'date', 'category')
Frequently Asked Questions (FAQ)
Q1: How do I create a grouped select dropdown in Django?
✔ Use **optgroup** in the **ChoiceField choices** or create a **custom GroupedModelChoiceField**.
Q2: Can Django ModelChoiceField group options?
✔ By default, no. You need a **custom ModelChoiceIterator** to generate grouped selections.
Q3: How do I exclude parent categories from being selectable?
✔ Use `Category.objects.exclude(parent=None)` when defining the queryset.