Formsets
TaskForm, as built in the previous lesson, handles exactly one task at a time. But
imagine setting up a brand-new TaskFlow project and wanting to add its first five tasks in one
sitting, on one page, without reloading between each one. A single form instance can't do that —
you need several copies of the same form rendered together, submitted together, and validated
together. That's precisely the problem a formset solves.
Building a Formset from TaskForm
A formset isn't a class you write by hand — it's a class Django builds for you, from a
form you've already defined, using a factory function. For a plain Form, that's
formset_factory(); for a ModelForm like TaskForm, it's
modelformset_factory():
# taskflow/forms.py
from django.forms import modelformset_factory
from .models import Task
TaskFormSet = modelformset_factory(
Task,
fields=['title', 'description', 'due_date', 'priority'],
extra=3,
)
TaskFormSet is now a class that, when instantiated, behaves like a list of
TaskForm-shaped forms bundled together — several tasks' worth of fields, all on one
page, all submitted in a single POST request.
The Management Form: Hidden, But Required
Alongside the individual task forms, every formset also renders a small set of hidden fields
called the management form — most importantly TOTAL_FORMS and
INITIAL_FORMS. These exist because Django needs to know, when the page comes back as
a POST request, exactly how many forms were rendered and how many of those represented existing
objects versus new blank ones. Without that count, Django would have no reliable way to tell how
many forms to expect back or which submitted data belongs to which form.
Rendering a Formset in a Template
A formset is iterated just like a list — looping over it yields each individual form, which is
then rendered the same way any single form would be. The management form is included
automatically as long as you render {{ formset.management_form }} once, somewhere
inside the <form> tag:
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
{{ form.as_p }}
{% endfor %}
<button type="submit">Save Tasks</button>
</form>
Validating and Saving a Formset
A formset is validated and saved as one unit, mirroring how a single ModelForm works — just at the level of the whole collection instead of one form:
# taskflow/views.py
from django.shortcuts import render, redirect
from .models import Task
from .forms import TaskFormSet
def task_bulk_create(request):
if request.method == 'POST':
formset = TaskFormSet(request.POST, queryset=Task.objects.none())
if formset.is_valid():
formset.save()
return redirect('task-list')
else:
formset = TaskFormSet(queryset=Task.objects.none())
return render(request, 'taskflow/task_bulk_create.html', {'formset': formset})
queryset=Task.objects.none() tells the formset there are no existing tasks to edit
here — every form on the page is meant for a brand-new Task. formset.is_valid()
checks every individual form, and formset.save() creates a real database row for
each one that has data, skipping any left completely blank.
Controlling Extra Forms and Deletion
Two options on modelformset_factory() shape how the formset behaves day to day:
extra sets how many blank forms appear beyond whatever existing objects are passed
in, and can_delete=True adds a delete checkbox to every form, letting a formset
remove existing rows as well as add new ones.
TaskFormSet = modelformset_factory(
Task,
fields=['title', 'description', 'due_date', 'priority'],
extra=1,
can_delete=True,
)
With extra=1, editing a project's existing three tasks would render three pre-filled
forms plus one blank form at the end for adding a fourth. With can_delete=True, each
form gains its own checkbox — ticking it and submitting deletes that specific task when
formset.save() runs.