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
- Building a formset with formset_factory
- Validating many forms together
- What the management form tracks and why it's required
- Editing model rows with modelformset_factory
- Editing related objects with inlineformset_factory
- Deleting rows with can_delete and the DELETE flag
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: OK2 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_FORMS3 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})- modelformset_factory — a grid of rows for one model.
- inlineformset_factory — child rows tied to one parent via a foreign key.
- instance=author — scopes the inline formset to that parent.
- can_delete=True — adds a DELETE checkbox per row.
# 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? TrueCommon 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
| Item | What 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=True | Add 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
- Previous: Custom User Model
- Next: Class-Based View Mixins — Compose views with LoginRequired, permission, and context mixins
- Quick reference: Django cheat sheet
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.