How to use Grouped Model Choice Field in Django?

Published On: 25/03/2025 | Category: Django
Django Grouped Model Choice Field Example

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.py
from 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 ChoiceField Example in Django

🔹 Example 2: Using Grouped Choice Field with Optgroup

To organize choices into **groups**, we use **optgroup** HTML tags:

forms.py
from 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 Grouped Choice Field in Django

🔹 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.py
from 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.py
from 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.py
from 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.