How to use Grouped Model Choice Field in Django?

Published on March 25, 2025 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.

Related Posts