skip to content

When editing an adoption application with a Django ModelForm, why must both the GET and the POST branches pass instance=?

level: middleimportance: should knowfreq 50%

answer

  1. where initial values come from
  2. insert versus update
  3. initial= wins over the instance
  4. fields off the form stay put
  5. fetch the row scoped to the user

basics

~20 s

In a Django ModelForm, instance= supplies the initial values on GET and tells save() which row to update on POST. A POST form built without instance= starts from a new object, so save() inserts a duplicate row instead of updating.

solid answer

~40 s

`instance=app` does two jobs. For display, the `ModelForm` builds its initial values from the instance with `model_to_dict()`, and an explicit `initial=` dict overrides those. For saving, cleaned values are applied onto that same instance, and because it already has a primary key, `save()` issues an update. If the POST branch builds `AdoptionApplicationForm(request.POST)` without `instance=`, Django creates a fresh `AdoptionApplication()`: the save inserts a second row, or fails on a missing required column such as `applicant`. Model fields not on the form keep their current values in an update, because `construct_instance()` only touches the form's fields. The instance should be fetched scoped to the user, for example with `get_object_or_404(AdoptionApplication, pk=pk, applicant=request.user)`.

code

python · 17 lines
python
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render

from .forms import AdoptionApplicationForm
from .models import AdoptionApplication

@login_required
def edit_application(request, pk):
    app = get_object_or_404(AdoptionApplication, pk=pk, applicant=request.user)
    if request.method == 'POST':
        form = AdoptionApplicationForm(request.POST, instance=app)   # update, not insert
        if form.is_valid():
            form.save()
            return redirect('applications:detail', pk=app.pk)
    else:
        form = AdoptionApplicationForm(instance=app)
    return render(request, 'applications/edit.html', {'form': form})

go deeper

for a junior

Recall that editing needs instance= on both requests, otherwise save() creates a new row.

for a middle

Explain model_to_dict for initial values, initial= precedence, and why fields off the form survive an update.

for a senior

Show you scope the instance lookup to the user, notice full-row overwrites of staff fields, and test that updates do not create rows.

for a principal

Consider how edit flows handle concurrent changes by staff and applicants, and where ownership checks live across views and APIs.

## What instance= does A `ModelForm` always works on a model instance, stored as `form.instance`. You either pass one or Django creates an empty one: - **With `instance=app`**: `self.instance = app`, and the form's initial data comes from `model_to_dict(app, fields, exclude)`. - **Without it**: `self.instance = AdoptionApplication()`, a new unsaved object, and initial data is empty. The same instance is where validation copies cleaned values and what `save()` writes. An instance with a primary key produces an **UPDATE**; a new one produces an **INSERT**. ## The edit view, done right 1. Fetch the row the user may edit: `app = get_object_or_404(AdoptionApplication, pk=pk, applicant=request.user)`. 2. On GET, build `AdoptionApplicationForm(instance=app)`; inputs show the stored values. 3. On POST, build `AdoptionApplicationForm(request.POST, instance=app)`. 4. If valid, `form.save()` updates that row and its many-to-many selections. The classic bug is step 3 without `instance=`. The form validates fine, and `save()` then tries to **insert** a brand-new application: here it fails with `IntegrityError` because `applicant`, which is not on the form, is empty; in a model without such a required column it quietly creates a duplicate row while the original stays unchanged. ## initial= versus the instance | Source | Used for | Precedence | |---|---|---| | `instance=app` | initial values of the form's fields, and the save target | lower | | `initial={'message': '...'}` | initial values | overrides the instance | | submitted `data` | the values that are validated and saved | the only input to saving | Passing `initial=` alongside `instance=` is a way to pre-fill a suggested change on an edit page, but only what the user submits is saved. ## What happens to fields the form does not include On an update, only the form's fields are copied onto the instance: - Model fields not in `Meta.fields` (`status`, `staff_notes`, `applicant`) keep their stored values and are written back unchanged when the instance is saved. - Fields with `editable=False` are never touched. - An optional field that has a model default and is simply absent from the POST keeps the instance's current value, except for checkbox and multi-select widgets, which browsers omit when empty, so absence there means "unticked" or "nothing selected". ## Related considerations - `save()` writes the whole row, so a concurrent staff change to `status` between loading and saving can be overwritten; restricting the write with `instance.save(update_fields=[...])` after `commit=False` is one mitigation. - The form cannot decide who may edit which application; scoping the lookup to `request.user`, or checking a permission, belongs in the view. ## Testing the edit flow 1. Post a valid change and assert the row count is unchanged, proving an update rather than an insert. 2. Assert that `status` and `staff_notes` still hold their stored values after the applicant's edit. 3. Request another user's application and expect a 404. 4. Post invalid data and assert the stored row did not change. ## What interviewers listen for - `instance=` on both GET and POST, and the insert-instead-of-update bug. - `initial=` overriding instance values for display only. - Fields off the form are preserved on update. - The row fetched with the user's scope, not by primary key alone.

  • On a Django ModelForm update, what happens to the staff_notes column if it is not in Meta.fields?
    It keeps its stored value. `construct_instance()` copies only the form's fields onto the instance, so `staff_notes` is written back as it was loaded. The risk is a concurrent change between load and save, which a full-row save can overwrite.
  • Why fetch the instance with get_object_or_404(..., applicant=request.user) rather than by pk alone in a Django edit view?
    A lookup by primary key lets any signed-in user edit anyone's application by changing the URL. Scoping the query to the current user makes other users' rows return 404. The ModelForm itself has no notion of ownership.

saying these in an interview costs you the question

  • instance= is only needed on GET to pre-fill the inputs.
  • Without instance=, save() updates the row whose values were displayed.
  • Fields not on the form are reset to their defaults on update.
  • initial= values are saved even if the user clears the input.
  • The ModelForm checks that the user owns the instance.