skip to content

In a Django ModelForm, what validation runs after the form's clean(), and why must an overridden clean() call super().clean()?

level: middleimportance: should knowfreq 44%

answer

  1. form first, then the model
  2. values copied onto the instance
  3. model clean() before uniqueness
  4. flags set by the parent clean
  5. errors merged into the form

basics

~20 s

After a Django ModelForm's clean(), _post_clean() copies cleaned data onto the instance, runs the model's field validation and clean(), then validate_unique() and validate_constraints(). Those last two only run if ModelForm.clean() ran, so overrides must call super().clean().

solid answer

~50 s

A `ModelForm` validates in two stages. First the ordinary form stage: field cleaning, `clean_<field>()` and the form's `clean()`. Then the internal `_post_clean()` hook: `construct_instance()` copies `cleaned_data` onto `self.instance`, the instance's `full_clean()` runs model field validation and the model's `clean()` for the fields on the form, and finally `validate_unique()` and `validate_constraints()` run. Those two are gated by flags that `ModelForm.clean()` sets, so an override that forgets `super().clean()` silently turns off uniqueness and constraint checks while everything else still works. Model errors are merged into the form: errors keyed by a form field land on that field, unkeyed ones become non-field errors, a key naming a field that is not on the form raises `ValueError`, and `Meta.error_messages` can replace messages by code. The instance is modified in memory even if validation fails, so it should not be reused.

code

python · 16 lines
python
from django import forms
from django.core.exceptions import ValidationError

from .models import AdoptionApplication

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

    def clean(self):
        cleaned = super().clean()   # sets the flags for validate_unique/validate_constraints
        if cleaned.get('home_type') == 'flat' and not cleaned.get('message'):
            self.add_error('message', ValidationError(
                'Tell us how a flat suits this pet.', code='flat_needs_message'))
        return cleaned

go deeper

for a junior

Recall that a ModelForm also runs the model's validation, and that an overridden clean() must call super().clean().

for a middle

Explain _post_clean's order: construct_instance, full_clean with the model's clean(), then uniqueness and constraints, and how model errors map to fields.

for a senior

Show you catch clean() overrides without super(), avoid reusing a mutated instance, and supply server-owned values before model rules need them.

for a principal

Decide which rules live on the model so every form and API shares them, and which belong to a particular form's workflow.

## Two validation stages Calling `is_valid()` on a `ModelForm` runs the normal form pipeline and then validates the model instance: 1. **Form stage**: each field's own cleaning, any `clean_<fieldname>()` methods, then the form's `clean()`. 2. **Model stage**, inside the internal `_post_clean()` hook: 1. `construct_instance()` copies the cleaned values of the form's fields onto `self.instance`. 2. `self.instance.full_clean(exclude=..., validate_unique=False, validate_constraints=False)` runs the model's field validation and the model's `clean()`. 3. `self.validate_unique()` runs the model's uniqueness checks, if enabled. 4. `self.validate_constraints()` runs `Meta.constraints` checks, if enabled. The `exclude` set keeps model validation away from fields that are not on the form or already failed. ## Why super().clean() matters Steps 3 and 4 are gated. `BaseModelForm.__init__` sets two private flags to False, and `ModelForm.clean()`, the parent implementation, sets them to True. Django's documentation warns about exactly this: override `clean()` without calling the parent, and uniqueness and constraint validation are skipped. | Your `clean()` | Field and model `clean()` validation | Unique and constraint checks | |---|---|---| | not overridden | run | run | | overridden, calls `super().clean()` | run | run | | overridden, no `super().clean()` | run | **skipped** | The failure is silent: the form validates, and a duplicate adoption application only surfaces as an `IntegrityError` at save time, or not at all if no database constraint backs it. ## How model errors reach the form The model stage raises `ValidationError`s that the form absorbs: - An error keyed by a field that is **on the form** is attached to that field. - An error from the model's `clean()` that is not tied to a field becomes a **non-field error**. - An error the model's `clean()` ties to a field that is **not on the form** cannot be attached anywhere, and Django raises `ValueError`. The model documentation suggests overriding `Model.clean_fields()`, which receives the excluded field names, for such rules. - If the form's `Meta.error_messages` or the field's `error_messages` has an entry for the error's `code`, such as `unique`, that text replaces the model's message. This lets one model-level rule, for example "applicants with other pets must describe them", show up on the right form field without duplicating the rule in every form. ## The instance is modified during validation Because `construct_instance()` runs before model validation, the instance passed with `instance=` carries the new values **in memory** after `is_valid()`, even when validation fails. Date strings have become `date` objects, choices have been applied, and so on. The documentation warns that a failed validation may leave the instance in an inconsistent state, so do not reuse it for display or further logic; re-fetch it if needed. Nothing is written to the database until `save()`. ## Ordering consequences worth knowing - The model's `clean()` runs **after** the form's `clean()`, so the form cannot see the model's errors while it runs. - The model's `clean()` runs **before** uniqueness and constraint checks. - Server-owned values set after `save(commit=False)` arrive **too late** for the model's `clean()`; pass them in through `instance=` or the form's `__init__` if model rules depend on them. ## Choosing between the form's clean() and the model's clean() - A rule that must hold **however** the object is written, from any form, the admin or a script calling `full_clean()`, belongs on the model. - A rule that belongs to **this workflow**, such as "the public form requires a message for flats", belongs in the form's `clean()`. - Remember that `Model.save()` does not call `full_clean()` by itself, so a model rule protects only code paths that validate; database constraints protect every path. ## What interviewers listen for The two stages, the `construct_instance()` step, the `super().clean()` warning, how model errors map onto form fields, and that the instance is mutated even on failure.

  • In a Django ModelForm, can the model's clean() see values the view sets after save(commit=False)?
    No. The model's `clean()` runs inside `is_valid()`, before the view gets the instance back from `save(commit=False)`. Values it depends on must be on the instance earlier, for example via `instance=Model(applicant=user)` or assigned in the form's `__init__`.
  • How can a Django ModelForm change the message of a model uniqueness error?
    Add an entry keyed by the error's code to `Meta.error_messages` for that field, for example `{'pet': {'unique': '...'}}`, or set it in the declared field's `error_messages`. When model errors are merged, Django replaces messages whose code matches, and `NON_FIELD_ERRORS` can be used as a key for form-wide ones.

saying these in an interview costs you the question

  • The model's clean() runs before the form's clean().
  • Skipping super().clean() in a ModelForm also skips the model's clean().
  • A failed is_valid() leaves the passed instance untouched.
  • Model validation errors are raised as exceptions from is_valid().
  • Model validation only happens when save() is called.
  • A model clean() error keyed to a field missing from the form shows as a non-field error.