In Django, what do Model.clean() and full_clean() do, and why doesn't save() call them?
answer
- validation versus persistence
- four steps in a fixed order
- cross-field rules and NON_FIELD_ERRORS
- forms call it, save() does not
basics
~10 sfull_clean() validates an instance: clean_fields(), then clean(), validate_unique() and validate_constraints(), raising one ValidationError. clean() is your hook for cross-field rules. save() never calls them, so code saving directly must validate itself.
solid answer
~40 s`full_clean()` is Django's model validation. It runs four steps in order, `clean_fields()` (each field's type, choices, `max_length`, `blank` and validators), `clean()` (your hook), `validate_unique()` and `validate_constraints()`, and raises a single `ValidationError` whose `message_dict` maps field names, or `NON_FIELD_ERRORS`, to messages. I override `clean()` for rules spanning fields, raising `ValidationError({"field": "..."})` to attach an error to a field, and it may also normalise values. `save()` deliberately does not call any of this: saving is persistence, and validation belongs where there is someone to show errors to. A `ModelForm` runs model validation during `is_valid()`, so the admin and form views get it, but `create()`, a script or a task that saves directly skips it unless it calls `full_clean()` itself.
code
python · 12 linesfrom django.core.exceptions import NON_FIELD_ERRORS, ValidationError
page = WikiPage(title=" Home ", slug="home", body="text", redirect_to=other_page)
page.save() # no validation: saved as-is, including the conflicting fields
try:
page.full_clean()
except ValidationError as exc:
print(exc.message_dict)
# {'body': ['A redirect page cannot have its own body.']}
print(exc.message_dict.get(NON_FIELD_ERRORS)) # None: the error is on 'body'go deeper
Recall that full_clean() validates a model instance, that clean() is where custom rules go, and that save() does not validate.
Explain the four steps of full_clean() in order, how message_dict and NON_FIELD_ERRORS work, and when ModelForm triggers it.
Decide how non-form code paths validate data, weigh calling full_clean() inside save(), and back critical rules with constraints.
Define where validation lives across forms, services and the database so every entry point enforces the same rules without duplicating them.
## Validation and saving are separate in Django Django splits two jobs that some frameworks merge: - **Validation** checks that an instance's values are acceptable and produces messages for a person to fix. - **Persistence** (`save()`) writes the instance to the database. `save()` never validates. `WikiPage(title="x" * 500).save()` sends the value straight to the database, which may reject it with a database error, or, on a backend that does not enforce lengths, store it. Validation happens only when something calls **`full_clean()`**. ## What full_clean() runs `Model.full_clean(exclude=None, validate_unique=True, validate_constraints=True)` runs four steps in a fixed order: 1. **`clean_fields()`**: each field's own checks: type conversion, `choices`, `max_length`, `blank=False`, and the field's `validators`. 2. **`clean()`**: an empty hook on `Model` that you override for model-wide rules. 3. **`validate_unique()`**: `unique=True`, `unique_for_date` and similar checks, run as queries. 4. **`validate_constraints()`**: the constraints declared in `Meta.constraints`. Errors from all steps are collected into one `ValidationError`. Its `message_dict` maps field names to lists of messages; errors not tied to a field go under the key `NON_FIELD_ERRORS`. The `exclude` argument skips named fields, which is how a form avoids reporting errors on fields it does not display. ## Writing clean() `clean()` is the place for rules that involve **more than one field**, or that need to adjust values before saving. For a wiki page that can be either a normal page or a redirect to another page: ```python from django.core.exceptions import ValidationError def clean(self): self.title = self.title.strip() if self.redirect_to_id and self.body: raise ValidationError( {"body": "A redirect page cannot have its own body."} ) ``` - Raising `ValidationError` with a **dict** attaches each message to a field, so a form shows it next to that input. - Raising it with a plain message puts it under `NON_FIELD_ERRORS`, shown at the top of a form. - Changing attributes (like stripping whitespace) is allowed: the modified values are what `save()` writes afterwards. ## Who calls full_clean() | Caller | Model validation runs? | |---|---| | `ModelForm.is_valid()` (admin, generic edit views) | yes, for the fields on the form | | `instance.full_clean()` in your code | yes | | `instance.save()` / `Manager.create()` | no | | `bulk_create()`, `update()`, fixtures | no | ## Should save() call full_clean()? Some teams override `save()` to call `self.full_clean()` first. The trade-offs: - It catches invalid data from scripts and tasks that bypass forms. - It runs validation twice for form-driven saves, including the uniqueness queries. - It raises `ValidationError` in places that have no user to show it to. - It still does nothing for `bulk_create()` and `update()`, which never call `save()`. A common middle ground is explicit validation at the service layer that creates objects outside forms, plus database constraints for rules that must hold no matter who writes. ## Where each kind of rule belongs | Rule | Model-side home | Enforced by `save()`? | |---|---|---| | a single value's format or range | the field's `validators`, `choices`, `max_length` | no, only by `full_clean()` | | a rule across several fields | `Model.clean()` | no, only by `full_clean()` | | uniqueness | `unique=True` or `UniqueConstraint` | yes, by the database; also checked by `full_clean()` | | a row-level invariant | `CheckConstraint` | yes, by the database; also checked by `full_clean()` | The pattern: validation methods give good messages but run only when called; database constraints always hold but give raw errors. Important rules usually get both. ## Mistakes interviewers look for - Believing `save()` enforces `choices` or `max_length`: the database may catch some of it, but model validation does not run. - Putting cross-field rules in a form's `clean()` only, so non-form code paths skip them; the model's `clean()` is shared by every `ModelForm` of that model. - Raising a plain `ValidationError` for a field-specific rule, so the message shows at the top of the form instead of next to the field. - Doing slow work in `clean()`, which runs on every form validation.
- How do you make a Django clean() error appear next to a specific form field rather than at the top?Raise `ValidationError` with a dictionary keyed by field name, such as `ValidationError({"body": "..."})`. A plain message goes under `NON_FIELD_ERRORS` and is shown as a form-wide error. If the named field is not on the `ModelForm`, Django raises `ValueError` because the error cannot be attached; the docs suggest overriding `clean_fields()` for that case.
- Why does WikiPage.objects.create(title="x" * 500) not raise a ValidationError in Django?`create()` calls `save()`, and `save()` never runs `full_clean()`. The too-long title goes straight to the database. On PostgreSQL the `varchar(200)` column rejects it with a database error, not a `ValidationError`; to get a friendly message, validate first with `full_clean()` or go through a `ModelForm`.
saying these in an interview costs you the question
- save() calls full_clean() automatically before writing the row.
- clean() runs before clean_fields() inside full_clean().
- Model validation runs for bulk_create() and QuerySet.update().
- clean() must never modify the instance's attribute values.
- A plain ValidationError in clean() attaches to the first field.