Formsets & Inline Formsets

Reviewed & published by Brayan K

A formset is a layer that manages several copies of the same form on one page, validating them as a group, so a user can add or edit many rows at once instead of submitting one form at a time.

Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll build a plain formset with formset_factory, understand the management form that tracks how many forms were submitted, and use modelformset_factory and inlineformset_factory to edit model rows and related child objects.

What You'll Learn in This Lesson

1 formset_factory — Managing Many Forms

A single Django form handles one record. A formset wraps a form class so you can render and validate many copies at once. You create the formset class with formset_factory, telling it the form and how many extra blank rows to show. When the page is submitted, the formset validates every form and is only valid if all of them are valid.

# forms.py
from django import forms
from django.forms import formset_factory

class BookForm(forms.Form):
    title = forms.CharField(max_length=200)
    pages = forms.IntegerField(min_value=1)

# A formset that shows 3 blank book forms
BookFormSet = formset_factory(BookForm, extra=3)

# views.py
def add_books(request):
    if request.method == "POST":
        formset = BookFormSet(request.POST)
        if formset.is_valid():
            for form in formset:
                print(form.cleaned_data)   # one dict per filled row
    else:
        formset = BookFormSet()
    return render(request, "books/add.html", {"formset": formset})
# formset_factory builds a class that manages MANY copies of one form.
# We simulate "validating a list of form dicts" the way a formset does:
# run each form's cleaning, collect errors, and report if ALL are valid.

def clean_book(data):
    errors = {}
    if not data.get("title"):
        errors["title"] = "This field is required."
    if data.get("pages", 0) <= 0:
        errors["pages"] = "Pages must be positive."
    return errors

def validate_formset(forms):
    all_errors = []
    for data in forms:
        all_errors.append(clean_book(data))
    is_valid = all(e == {} for e in all_errors)
    return is_valid, all_errors

forms = [
    {"title": "Django 101", "pages": 120},
    {"title": "", "pages": 0},          # two errors
    {"title": "ORM Deep Dive", "pages": 80},
]

valid, errors = validate_formset(forms)
print("formset valid?", valid)
for i, e in enumerate(errors):
    print(f"  form {i}: {e if e else 'OK'}")

# EXPECTED:
# formset valid? False
#   form 0: OK
#   form 1: {'title': 'This field is required.', 'pages': 'Pages must be positive.'}
#   form 2: OK

# ✅ Expected output:
# formset valid? False
#   form 0: OK
#   form 1: {'title': 'This field is required.', 'pages': 'Pages must be positive.'}
#   form 2: OK

2 The Management Form

A formset needs to know how many forms were submitted. It tracks this with a hidden management form — a few fields named TOTAL_FORMS, INITIAL_FORMS, MIN_NUM_FORMS, and MAX_NUM_FORMS. You must render {{ formset.management_form }} in your template, or the submitted data won't include these counters and Django will raise a validation error.

<!-- books/add.html -->
<form method="post">
  {% csrf_token %}
  {{ formset.management_form }}   {# <-- REQUIRED, easy to forget #}
  {% for form in formset %}
    {{ form.as_p }}
  {% endfor %}
  <button type="submit">Save</button>
</form>
# The MANAGEMENT FORM is hidden bookkeeping that tells Django how many
# forms were submitted. Without it, the formset can't reconstruct itself
# and raises a ValidationError. We simulate parsing it from POST data.

def parse_management_form(post):
    required = ["TOTAL_FORMS", "INITIAL_FORMS"]
    for key in required:
        if key not in post:
            raise ValueError(
                "ManagementForm data is missing or has been tampered with. "
                f"Missing key: {key}"
            )
    return {
        "total": int(post["TOTAL_FORMS"]),
        "initial": int(post["INITIAL_FORMS"]),
    }

# A well-formed POST includes the management form fields:
good_post = {
    "TOTAL_FORMS": "3",
    "INITIAL_FORMS": "1",
    "form-0-title": "A", "form-1-title": "B", "form-2-title": "C",
}
info = parse_management_form(good_post)
print("Rendering", info["total"], "forms;", info["initial"], "already in the DB")

# A POST that forgot the management form blows up the way Django's does:
try:
    parse_management_form({"form-0-title": "A"})
except ValueError as e:
    print("Error:", e)

# EXPECTED:
# Rendering 3 forms; 1 already in the DB
# Error: ManagementForm data is missing or has been tampered with. Missing key: TOTAL_FORMS

# ✅ Expected output:
# Rendering 3 forms; 1 already in the DB
# Error: ManagementForm data is missing or has been tampered with. Missing key: TOTAL_FORMS

3 modelformset_factory and inlineformset_factory

The two model-aware variants save you from writing forms by hand. modelformset_factory builds a formset bound to one model, so you can edit a grid of rows from that table. inlineformset_factory is for editing the children of a parent through a foreign key — like all the Book rows that belong to one Author — and it sets each child's foreign key to the parent automatically.

# forms.py
from django.forms import modelformset_factory, inlineformset_factory
from .models import Author, Book

# Edit many Book rows at once
BookModelFormSet = modelformset_factory(
    Book, fields=["title", "pages"], extra=2, can_delete=True
)

# Edit the Books that belong to ONE Author
BookInlineFormSet = inlineformset_factory(
    Author, Book, fields=["title", "pages"], extra=1, can_delete=True
)

# views.py
def edit_author_books(request, pk):
    author = Author.objects.get(pk=pk)
    if request.method == "POST":
        formset = BookInlineFormSet(request.POST, instance=author)
        if formset.is_valid():
            formset.save()   # FK to author set for you; deletions applied
    else:
        formset = BookInlineFormSet(instance=author)
    return render(request, "authors/edit.html", {"formset": formset})
# inlineformset_factory edits CHILD objects tied to a PARENT.
# Example: one Author (parent) with many Book children. The inline
# formset attaches each child to the parent and supports deletion.
# We simulate saving the children and honoring a DELETE flag.

def save_inline_formset(parent_id, child_forms):
    saved, deleted = [], []
    for form in child_forms:
        if form.get("DELETE"):
            deleted.append(form["title"])
            continue
        child = {"title": form["title"], "author_id": parent_id}  # FK set for you
        saved.append(child)
    return saved, deleted

author_id = 7
child_forms = [
    {"title": "Old Title", "DELETE": True},      # marked for deletion
    {"title": "New Title", "DELETE": False},
    {"title": "Another Book", "DELETE": False},
]

saved, deleted = save_inline_formset(author_id, child_forms)
print("Saved children (FK auto-set):")
for c in saved:
    print(" ", c)
print("Deleted:", deleted)

# EXPECTED:
# Saved children (FK auto-set):
#   {'title': 'New Title', 'author_id': 7}
#   {'title': 'Another Book', 'author_id': 7}
# Deleted: ['Old Title']

# ✅ Expected output:
# Saved children (FK auto-set):
#   {'title': 'New Title', 'author_id': 7}
#   {'title': 'Another Book', 'author_id': 7}
# Deleted: ['Old Title']

🎯 Your Turn: Validate the Whole Formset

A formset is valid only when every form is valid. Replace the blank so is_valid is True only when no form has errors.

# 🎯 YOUR TURN: a formset is valid only if EVERY form is valid.
# Replace ___ so is_valid is True only when no form has errors.

form_errors = [{}, {}, {}]   # three clean forms -> should be valid

is_valid = ___

print("formset valid?", is_valid)

# EXPECTED OUTPUT:
# formset valid? True

Common Errors (And How to Fix Them)

❌ "ManagementForm data is missing or has been tampered with"

You rendered the forms but forgot the hidden management form.

✅ Fix: add {{ formset.management_form }} inside the <form> tag.

❌ My inline formset rows aren't linked to the parent

You didn't pass the parent instance, so the foreign key was never set.

✅ Fix: construct the inline formset with instance=parent.

❌ Deleting a row in the form doesn't actually delete it

The DELETE field only appears when you opt in.

✅ Fix: pass can_delete=True to the factory and call formset.save().

📋 Quick Reference

ItemWhat It Does
formset_factory(Form, extra=3)Build a formset of plain forms with 3 blank rows
modelformset_factory(Model, ...)Edit many rows of one model at once
inlineformset_factory(Parent, Child, ...)Edit child rows tied to a parent via a foreign key
{{ formset.management_form }}Hidden fields tracking how many forms were submitted
can_delete=TrueAdd a DELETE checkbox to each row

🎯 Mini Challenge: Build a Reusable Formset Validator

Write a factory that returns a validator for a list of forms, then use it to validate a list of Product dicts (name required, price positive) and report a summary.

# 🎯 Mini Challenge: a mini modelformset_factory-style validator.
# Build a formset that validates a list of "Product" dicts: name required,
# price > 0. Report total, valid count, and per-form errors.

def make_formset(clean_fn):
    def run(forms):
        results = [clean_fn(f) for f in forms]
        valid_count = sum(1 for e in results if not e)
        return {
            "total": len(forms),
            "valid": valid_count,
            "all_valid": valid_count == len(forms),
            "errors": results,
        }
    return run

def clean_product(data):
    errors = {}
    if not data.get("name"):
        errors["name"] = "Name is required."
    if data.get("price", 0) <= 0:
        errors["price"] = "Price must be positive."
    return errors

ProductFormSet = make_formset(clean_product)

report = ProductFormSet([
    {"name": "Pen", "price": 1.5},
    {"name": "", "price": 2.0},
    {"name": "Glue", "price": 0},
])

print("total forms:", report["total"])
print("valid forms:", report["valid"])
print("all valid?", report["all_valid"])
for i, e in enumerate(report["errors"]):
    print(f"  form {i}:", e if e else "OK")

# EXPECTED:
# total forms: 3
# valid forms: 1
# all valid? False
#   form 0: OK
#   form 1: {'name': 'Name is required.'}
#   form 2: {'price': 'Price must be positive.'}

❓ Frequently Asked Questions

Lesson complete — you can edit many records at once!

You built a formset with formset_factory, learned why the management form is required, and used modelformset_factory and inlineformset_factory to edit model rows and the related children of a parent — including deletions.

Practice quiz

What does a formset manage?

  • A single form's fields
  • Several copies of the same form on one page
  • Database migrations
  • URL routing

Answer: Several copies of the same form on one page. A formset manages multiple copies of the same form so users can fill many rows at once.

Which factory builds a basic formset from a form class?

  • modelformset_factory
  • inlineformset_factory
  • form_collection
  • formset_factory

Answer: formset_factory. formset_factory takes the form class and options like the number of extra blank forms.

Which factory option controls how many blank forms are shown?

  • blank
  • extra
  • spare
  • rows

Answer: extra. The 'extra' argument sets how many empty forms appear.

What must you render in the template to avoid a ManagementForm error?

  • {{ formset.errors }}
  • {{ formset.media }}
  • {{ formset.management_form }}
  • {{ formset.as_table }}

Answer: {{ formset.management_form }}. The management form holds TOTAL_FORMS and INITIAL_FORMS counters Django needs.

Which hidden field reports how many forms were submitted?

  • TOTAL_FORMS
  • FORM_COUNT
  • NUM_FORMS
  • SUBMITTED

Answer: TOTAL_FORMS. TOTAL_FORMS, in the management form, tells Django how many forms came back.

Which factory edits many rows of a single model?

  • formset_factory
  • modelformset_factory
  • inlineformset_factory
  • modelform_factory

Answer: modelformset_factory. modelformset_factory creates a formset bound to one model for editing many rows.

Which factory edits child objects related to one parent by foreign key?

  • formset_factory
  • modelformset_factory
  • inlineformset_factory
  • parent_factory

Answer: inlineformset_factory. inlineformset_factory edits child rows tied to a parent and sets the foreign key for you.

What does inlineformset_factory do with the foreign key to the parent?

  • Ignores it
  • Requires you to set it manually
  • Deletes it
  • Sets it automatically on each child

Answer: Sets it automatically on each child. The inline formset automatically links each child to the parent and sets the FK.

How do you let users delete rows in a formset?

  • Pass can_delete=True to the factory
  • Override the delete() method
  • Set extra=0
  • Add a delete button manually only

Answer: Pass can_delete=True to the factory. can_delete=True adds a DELETE checkbox; checked forms are removed on save.

Which option adds an ORDER field so users can reorder rows?

  • can_sort=True
  • can_order=True
  • ordered=True
  • sortable=True

Answer: can_order=True. can_order=True adds an ORDER field for reordering rows.

Continue this course

Frequently asked questions

What is a formset in Django?

A formset is a layer that manages several copies of the same form on one page. Instead of rendering and validating a single form, a formset lets a user fill in many rows at once — for example five blank lines to add five products. You build one with formset_factory, passing the form class and options like how many extra blank forms to show. The formset validates every form together and reports the results as a group.

What is the management form and why do I keep getting errors about it?

The management form is a small set of hidden fields — TOTAL_FORMS, INITIAL_FORMS, MIN_NUM_FORMS, and MAX_NUM_FORMS — that Django uses to know how many forms were submitted and how many already existed. If you render a formset's individual forms but forget to output {{ formset.management_form }} in the template, the submitted data is missing those counters and Django raises a ValidationError saying the ManagementForm data is missing or tampered with. The fix is always to include the management form in the template.

What is the difference between modelformset_factory and inlineformset_factory?

modelformset_factory creates a formset of forms bound to a single model, so you can edit many rows of one table at once — like a grid of all your products. inlineformset_factory is for editing the child objects of one parent through a foreign key, like editing all the Book rows that belong to a single Author. The inline version automatically links each child to the parent and sets the foreign key for you, so you never have to assign it by hand.

How do I let users delete rows in a formset?

Pass can_delete=True to the factory. Django then adds a DELETE checkbox to each form. When the formset is saved, any form whose DELETE box is checked is removed instead of saved. In an inline formset this deletes the corresponding child object. You can also pass can_order=True to add an ORDER field if you want users to reorder the rows.