skip to content

In a Django ModelForm, what does save(commit=False) return, and why must you call save_m2m() afterwards?

level: middleimportance: must knowfreq 66%

answer

  1. an instance not yet in the database
  2. fill in server-owned fields
  3. relations need a primary key
  4. a method attached after the call
  5. commit=True does both

basics

~10 s

Django's ModelForm.save(commit=False) returns the model instance populated from cleaned_data but not saved. After setting extra fields and calling instance.save(), you call form.save_m2m(), because many-to-many rows need the saved instance's primary key.

solid answer

~40 s

`save(commit=False)` returns the form's model instance with the cleaned values applied, without touching the database, so the view can fill in what the user must not supply: `app.applicant = request.user`, `app.status = 'pending'`. Then `app.save()` writes the row. Many-to-many selections, such as `preferred_traits`, cannot be saved before the row exists, because the link table needs its primary key; so when `commit=False` is used, Django attaches a `save_m2m()` method to the form, and you call `form.save_m2m()` after saving the instance. Forgetting it silently drops the selections. With the default `commit=True`, `save()` saves the instance and the many-to-many data in one call. Either way `save()` raises `ValueError` if the form has errors.

code

python · 16 lines
python
from django import forms

from .models import AdoptionApplication

class AdoptionApplicationForm(forms.ModelForm):
    class Meta:
        model = AdoptionApplication
        fields = ['pet', 'home_type', 'has_other_pets', 'message', 'preferred_traits']

    def save_for(self, user):
        app = super().save(commit=False)
        app.applicant = user
        app.status = AdoptionApplication.Status.PENDING
        app.save()
        self.save_m2m()          # without this, preferred_traits is lost
        return app

go deeper

for a junior

Recall the four-step sequence: save(commit=False), set extra fields, instance.save(), form.save_m2m().

for a middle

Explain that validation already applied values to the instance, why many-to-many data needs a primary key, and when save_m2m appears on the form.

for a senior

Show you prevent the silent many-to-many loss by wrapping the sequence in the form or passing instance=, and think about the transaction around both writes.

for a principal

Decide where server-owned values are assigned across a codebase so views cannot diverge, and make that pattern easy to follow.

## The default: save() does everything On a valid `ModelForm`, `form.save()` (with the default `commit=True`) does two things in order: 1. `instance.save()`: inserts or updates the model's row. 2. `_save_m2m()`: writes many-to-many selections, such as the traits an applicant would like in a pet, into the link table. It returns the saved instance. If the form has errors, `save()` raises `ValueError` saying the object could not be created (or changed) because the data did not validate. ## What commit=False changes `form.save(commit=False)` returns the **same instance** with every cleaned value already applied, but performs **no** database write. (The values were copied onto the instance during `is_valid()`, in the form's model-validation step.) This is the hook for values the user must not provide: - **Server-owned fields**: `applicant`, a submitted-at timestamp, an initial `status`. - **Derived values**: a slug or a score computed from several fields. - **Special save options**: calling `instance.save()` with arguments such as `update_fields` or `force_insert` yourself. Because no row exists yet, Django cannot write many-to-many data. Instead it **attaches a `save_m2m` method** to the form object during that call. The full sequence for the adoption form is: 1. `app = form.save(commit=False)` 2. `app.applicant = request.user` 3. `app.save()` 4. `form.save_m2m()` ## The silent failure Skipping step 4 raises nothing. The application row is saved, the page redirects, and the applicant's `preferred_traits` selections vanish. Symptoms surface later as "the form ignores my choices", usually reported only for many-to-many fields, which is the clue. | Call | Row saved | Many-to-many saved | `save_m2m` available | |---|---|---|---| | `form.save()` | yes | yes | not needed | | `form.save(commit=False)` | no | no | yes, call after `instance.save()` | | `form.save(commit=False)` then `instance.save()` only | yes | **no** | yes, but forgotten | ## Alternatives worth knowing - **Pass the server-owned values in up front**: `AdoptionApplicationForm(request.POST, instance=AdoptionApplication(applicant=request.user))`. Then a plain `form.save()` works and the value is set before model validation runs, although a field that is not on the form is still excluded from the form's uniqueness checks. - **Override `save()` on the form** to accept the extra values and call `super().save(commit=False)`, set them, save, and call `self.save_m2m()`, so views cannot forget. - Consider wrapping the instance save and the many-to-many save in one transaction so a failure between them does not leave a row without its selections; that is the transactions topic. ## Mistakes that show up in review - Calling `form.save_m2m()` after a plain `form.save()`: the method is only attached by a `commit=False` call, so this raises `AttributeError` (and would be redundant anyway). - Calling `save_m2m()` before `instance.save()`: the instance has no primary key yet. - Using `commit=False` only to set a value that could have been passed in with `instance=`, which adds steps and the chance to forget one. - Expecting `save_m2m()` to save relations the form does not include: it writes only the many-to-many fields that are on the form. ## What interviewers listen for - `commit=False` returns an unsaved, fully populated instance; validation has already happened. - Many-to-many data needs a primary key, hence `save_m2m()`. - `save_m2m` exists only after a `commit=False` call. - `save()` refuses an invalid form with `ValueError`.

  • In Django, does save(commit=False) on a ModelForm skip validation?
    No. `save()` requires a valid form: it checks `form.errors`, which runs validation if needed, and raises `ValueError` when there are errors. `commit=False` only postpones the database write; the cleaned values were already applied to the instance during validation.
  • What happens if you call form.save_m2m() on a Django ModelForm before saving the instance?
    The instance has no primary key yet, so writing to the many-to-many link table fails; Django refuses to use a relation on an unsaved instance. The order must be `form.save(commit=False)`, then `instance.save()`, then `form.save_m2m()`.
  • Why might you prefer passing instance=Model(applicant=user) to a Django ModelForm over setting applicant after commit=False?
    The value is present on the instance before model validation, so a model `clean()` that looks at it sees the real applicant, and the view can use plain `form.save()`, which also saves many-to-many data. It does not make the form validate uniqueness involving `applicant`, because fields not on the form stay excluded from those checks.

saying these in an interview costs you the question

  • save(commit=False) skips form and model validation.
  • commit=False saves the row but postpones only the many-to-many data.
  • save_m2m() is needed after every form.save() call.
  • Forgetting save_m2m() raises an error you will notice.
  • save() on an invalid form saves the fields that did validate.