skip to content

Formsets & Inline Sets

formset_factory repeats one form N times, tracked by a hidden management form, and inlineformset_factory edits a parent's children in one POST. Interviewers probe the missing ManagementForm error.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

With Django's inlineformset_factory, how do you save an order and its line items from one POST so every line references the order?

level: middleimportance: must knowfreq 52%

answer

  1. a model formset tied to one parent
  2. validate both before saving either
  3. save the parent first
  4. formset.instance gets the saved parent
  5. commit=False skips deletions

basics

~20 s

Bind a ModelForm for the order and an inline formset with instance=order to request.POST, validate both, save the order, set formset.instance to it and call formset.save(), which stamps each new line with the order's key.

solid answer

~40 s

`inlineformset_factory(Order, OrderLine, fields=["sku", "quantity"])` builds a model formset limited to one parent's children: with `instance=order` its queryset is the order's lines, and the foreign key is replaced on each form by a hidden `InlineForeignKeyField` that must match the parent. In the view, bind the order's `ModelForm` and the formset to `request.POST`, both with the same `instance`, and validate **both** before saving anything. Then save the order, set `formset.instance = order` and call `formset.save()` — the inline formset assigns `formset.instance` to each new line just before saving it — ideally inside `transaction.atomic()`. `formset.save()` also deletes lines ticked `DELETE` (inline formsets default to `can_delete=True` and `extra=3`), but with `commit=False` nothing is deleted: you loop `formset.deleted_objects` yourself.

code

python · 36 lines
python
from django.db import transaction
from django.forms import ModelForm, inlineformset_factory
from django.shortcuts import get_object_or_404, redirect, render

from .models import Order, OrderLine


class OrderForm(ModelForm):
    class Meta:
        model = Order
        fields = ["delivery_note"]


OrderLineFormSet = inlineformset_factory(
    Order, OrderLine, fields=["sku", "quantity"], extra=1
)


def edit_order(request, pk=None):
    if pk is None:
        order = Order(customer=request.user)
    else:
        order = get_object_or_404(Order, pk=pk, customer=request.user)
    if request.method == "POST":
        form = OrderForm(request.POST, instance=order)
        formset = OrderLineFormSet(request.POST, instance=order)
        if form.is_valid() and formset.is_valid():
            with transaction.atomic():
                order = form.save()
                formset.instance = order
                formset.save()
            return redirect("order-detail", pk=order.pk)
    else:
        form = OrderForm(instance=order)
        formset = OrderLineFormSet(instance=order)
    return render(request, "orders/edit.html", {"form": form, "formset": formset})

go deeper

for a junior

Recall that inlineformset_factory takes the parent model, the child model and fields, and that the view passes instance=order on both GET and POST.

for a middle

Explain the validate-both, save-parent, reassign-instance, save-formset sequence and what formset.save() does with changed, new, untouched and deleted rows.

for a senior

Catch the commit=False deletion trap, wrap the two saves in one transaction, and scope the parent lookup to the user before binding the formset.

for a principal

Weigh one inline formset page against separate line endpoints when orders grow large or several people edit them at once, and decide where cross-line rules are enforced.

## What an inline formset is `inlineformset_factory(Order, OrderLine, fields=[...])` builds a **model formset** bound to one parent object: give it `instance=order` and it edits exactly the `OrderLine` rows whose foreign key points at that order. Internally it finds the foreign key from `OrderLine` to `Order`, calls `modelformset_factory`, and filters the formset's queryset by that key. Three things make it different from a plain model formset: - **The foreign key is not a user field.** Django replaces it on every form with an `InlineForeignKeyField` — a hidden field whose posted value must match the parent's key, or the form fails with "The inline value did not match the parent instance." A user cannot move a line to another order through the form. - **Different defaults.** `inlineformset_factory` uses `extra=3` and `can_delete=True`; `formset_factory` and `modelformset_factory` use `extra=1` and `can_delete=False`. - **The prefix** defaults to the reverse accessor name: `lines` when the key declares `related_name="lines"`, otherwise `orderline_set`. If `OrderLine` has two foreign keys to `Order`, the factory raises `ValueError` ("… has more than one ForeignKey to …") until you pass `fk_name=`. If the key is unique, as with a `OneToOneField`, the factory forces `max_num=1`. ## Create and edit in one view The view in the code example serves both pages: with a `pk` it loads the order, without one it starts from an unsaved `Order()`. The order of operations is the whole point: 1. **Bind both** the order's `ModelForm` and the formset to `request.POST`, each with the same `instance`. 2. **Validate both before saving either.** If the lines are invalid, no order may have been written yet. 3. **Save the parent first**, so it has a primary key. 4. **Point the formset at the saved parent** with `formset.instance = order`, then call `formset.save()`. The inline formset's `save_new()` assigns `formset.instance` to each new line just before saving it, so every new line gets the order's key. 5. Wrap steps 3–4 in `transaction.atomic()` so a failure while saving lines does not leave an order with no lines. On a create page the parent is unsaved, so the formset has no existing rows — its queryset is empty — and shows only `extra` blank forms. If you pass no `instance` at all, the formset creates an empty parent object of its own, which is exactly why step 4 reassigns `formset.instance` to the object you saved. ## What formset.save() does `formset.save()` handles existing rows first, then new rows: | row kind | condition | action with `commit=True` | |---|---|---| | existing, ticked `DELETE` | `can_delete=True` | deleted; listed in `formset.deleted_objects` | | existing, changed | `form.has_changed()` | saved; listed in `formset.changed_objects` | | existing, unchanged | — | left alone | | extra, filled in | not ticked for deletion | created; listed in `formset.new_objects` | | extra, untouched | — | ignored | It returns the list of changed and newly created instances. ## The commit=False trap With `formset.save(commit=False)` you get unsaved instances back — useful for setting a computed `unit_price` on each line — but **nothing is deleted**. The lines the user ticked are listed in `formset.deleted_objects`, and deleting them is your job. If the line form has many-to-many fields, call `formset.save_m2m()` after saving the instances. ```python lines = formset.save(commit=False) for line in lines: line.unit_price = current_price(line.sku) # your pricing lookup line.save() for line in formset.deleted_objects: line.delete() ``` ## Letting users reorder lines Pass `can_order=True` and each form gains an `ORDER` integer field, pre-filled with 1, 2, 3 … for existing rows. After `is_valid()`, `formset.ordered_forms` returns the forms sorted by that value, leaving out forms ticked for deletion and untouched spare forms; a blank `ORDER` sorts last. `ORDER` is a form field, not a model field, so persisting the sequence is your job, for example after `formset.save()`: ```python formset.save() for position, form in enumerate(formset.ordered_forms, start=1): form.instance.position = position form.instance.save(update_fields=["position"]) ``` Reading `ordered_forms` on a formset built without `can_order`, or on one that is not valid, raises `AttributeError`. ## Common bugs - **Passing `instance=order` on GET but not on POST.** The POST formset is then bound to a blank parent, its posted lines no longer match that parent, and the formset does not validate. - **Saving the order, then validating the formset.** An invalid formset leaves a saved order behind. - **Omitting `{{ formset.management_form }}`** from a hand-written template, which makes every POST invalid. - **Treating untouched spare rows as errors.** They are ignored on save; only the first `min_num` rows must be filled. - **Calling `save(commit=False)` and forgetting `deleted_objects`**, so ticked lines never disappear.

  • What changes if you call formset.save(commit=False)?
    You get the changed and new line instances back unsaved, so you can set extra attributes before calling `save()` on each. Nothing is deleted: the lines ticked for deletion are only listed in `formset.deleted_objects`, and you must delete them yourself. Call `formset.save_m2m()` afterwards if the line form has many-to-many fields.
  • What happens if OrderLine has two foreign keys to Order?
    `inlineformset_factory` cannot tell which one links the lines to the order and raises `ValueError` saying the model has more than one `ForeignKey` to `Order`. Pass `fk_name="order"` to name the key the formset should follow.
  • Can a user move a line to a different order by editing the hidden foreign key?
    No. The inline formset replaces the key on each form with an `InlineForeignKeyField` whose posted value must equal the parent's key; a different value fails with "The inline value did not match the parent instance." The parent itself must still be loaded with the user's scope in the view.

saying these in an interview costs you the question

  • save the order first, then validate the line formset
  • formset.save(commit=False) still deletes the lines ticked for deletion
  • the instance argument is needed only on the GET branch
  • users can pick each line's order from a visible dropdown in an inline formset
  • Django defers the order's INSERT until the lines are saved
open as a page

In Django, why does a submitted formset report 'ManagementForm data is missing or has been tampered with', and how do you fix it?

level: middleimportance: must knowfreq 55%

basics

~20 s

A bound Django formset reads its form count from hidden <prefix>-TOTAL_FORMS and <prefix>-INITIAL_FORMS fields. The error means they were not posted under the expected prefix: render {{ formset.management_form }} and use the same prefix on GET and POST.

open as a page

In Django, what is a formset, and how do formset_factory's extra, min_num and max_num decide how many forms it renders?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A Django formset is a class, built by formset_factory, that repeats one form on a page and validates the copies together. Unbound, it renders max(initial, min_num) + extra forms, trimmed to max_num unless the initial rows alone exceed it.

open as a page

In a Django formset, where do you put validation that spans several forms, and how do its errors reach the template?

level: middleimportance: should knowfreq 38%

basics

~20 s

Cross-form rules go in clean() on a BaseFormSet subclass passed to the factory with formset=. It runs after every form is cleaned; a ValidationError raised there lands in formset.non_form_errors(), which the template must render explicitly.

open as a page

With Django's modelformset_factory, how do you stop a bulk-edit page from changing rows outside the user's scope or creating rows it never offered?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Pass a user-scoped queryset to the model formset on both GET and POST, because without one it covers every row of the model; and use edit_only=True, since extra=0 or max_num does not stop a client posting new rows.

open as a page