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.

Note: Never remove or edit these hidden management-form fields in your template. If they're missing or altered, Django raises a validation error rather than guessing — it treats an untrustworthy count as a hard stop, not something to work around silently.

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.

Note: Formsets genuinely have a steeper learning curve than a single ModelForm, and the management form's bookkeeping trips people up the first few times. Reach for a formset specifically when a page needs "several of the same form at once" — bulk task entry, editing a whole project's tasks together — not as a default replacement for an ordinary single-object form.
Next up: Once TaskFlow has hundreds of tasks, rendering all of them on one page becomes a real problem of its own — Pagination, next.