In Django forms, in what order does is_valid() run to_python(), validate(), validators, clean_<field>() and clean()?
answer
- one field at a time first
- Field.clean() wraps three steps
- the per-field hook needs success
- form-wide hook runs regardless
- return value replaces cleaned_data
basics
~10 sDjango's is_valid() runs full_clean(): for each field in declared order, Field.clean() calls to_python(), validate() and run_validators(), then clean_<field>() if that passed. After every field, the form's clean() runs, even if fields failed.
solid answer
~40 s`is_valid()` (or reading `form.errors`) triggers `full_clean()` once. For each field in declaration order, Django calls `Field.clean()`, which runs `to_python()` to coerce the raw value, `validate()` for field-type rules such as `required`, and `run_validators()`; the first of those three to raise stops that field. If the field cleaned successfully, Django calls the form's `clean_<fieldname>()`, whose **return value** replaces the entry in `cleaned_data`, so forgetting `return` stores `None`. Other fields keep being cleaned whatever happened before. Once every field is done, `Form.clean()` runs, even when some fields failed, which is why it must use `cleaned_data.get()`. Errors raised there become non-field errors. A `ModelForm` then runs model validation in an internal hook.
code
python · 16 linesfrom django import forms
class BookingForm(forms.Form):
check_in = forms.DateField()
check_out = forms.DateField()
guests = forms.IntegerField(min_value=1, max_value=4)
promo_code = forms.CharField(required=False, max_length=12)
def clean_promo_code(self):
code = self.cleaned_data['promo_code'].strip().upper()
return code # without this line cleaned_data['promo_code'] is None
def clean(self):
cleaned = super().clean()
# runs even if 'guests' failed; failed fields are absent here
return cleanedgo deeper
Recall the three layers: field cleaning, then clean_<field>(), then the form's clean(). Remember that clean_<field>() must return the value.
Explain Field.clean()'s to_python, validate, run_validators chain, why per-field hooks need a successful field, and why clean() still runs after failures.
Show you would catch order-dependent clean_<field>() logic, KeyError-prone clean() code and missing returns in review, and test each path.
Discuss how a team keeps validation predictable: which layer owns which rule, and how that maps onto model validation for ModelForms.
## The entry point A Django form validates lazily. Nothing runs when you construct it; the first call to `is_valid()`, or the first read of `form.errors`, calls **`full_clean()`**, and the result is cached for the life of the instance. `full_clean()` does three things in sequence: 1. `_clean_fields()`: clean every field, one at a time, in the order the fields are declared. 2. `_clean_form()`: call the form's `clean()` method once. 3. `_post_clean()`: an internal hook that does nothing on a plain `Form`; `ModelForm` uses it to run model validation. An unbound form skips all of this, and `is_valid()` is simply `is_bound and not errors`. ## Inside Field.clean() For each field, `Field.clean(value)` runs three methods, and the first one to raise a `ValidationError` stops the chain for that field: | Step | Method | Job | Returns | |---|---|---|---| | 1 | `to_python()` | coerce the raw string into a Python type | the converted value | | 2 | `validate()` | field-type rules that are not validators, such as `required` | nothing | | 3 | `run_validators()` | run every callable in `field.validators` | nothing | `run_validators()` differs from the other two: it skips empty values entirely, and it runs **all** validators, collecting their errors into one `ValidationError`, so a field can report several problems at once. ## clean_<fieldname>() If `Field.clean()` succeeded, Django looks for a method named `clean_<fieldname>()` on the form and calls it with no arguments: - The value is already in `self.cleaned_data[fieldname]` as a Python object, not the raw string. - The method's **return value replaces** that entry. A method that normalises the value but forgets `return` stores `None`. - If `Field.clean()` failed, `clean_<fieldname>()` is **not** called for that field. - Only fields declared earlier and already cleaned successfully are in `cleaned_data` at this moment, so a per-field method should not depend on its neighbours. - A failure in one field does not stop the next field from being cleaned. ## Form.clean() After every field has been processed, Django calls `clean()` **whether or not earlier steps raised errors**. That is the documented home for rules that span fields, such as check-out after check-in. Because failed fields have been removed from `cleaned_data`, code here reads values with `.get()` and guards against `None`. A `ValidationError` raised from `clean()` is stored as a non-field error under the key `'__all__'`; `self.add_error(field, error)` attaches one to a specific field instead. `clean()` may return a new dictionary, which becomes `cleaned_data`; returning `None` keeps the existing one. ## Tracing a hotel booking form For a form declaring `check_in`, `check_out`, `guests` and `promo_code`, with `clean_promo_code()` and `clean()` defined, a submission with an invalid `guests` value runs: 1. `check_in`: `to_python`, `validate`, validators; success. 2. `check_out`: the same; success. 3. `guests`: `to_python` raises "Enter a whole number."; the field is skipped from `cleaned_data`. 4. `promo_code`: field clean succeeds, then `clean_promo_code()` uppercases and returns the code. 5. `clean()`: runs, sees both dates, compares them. The user gets every error in one round trip rather than one per submission. ## Customising the pipeline safely Each step is a method you may override, but the documentation draws clear lines: - Override `to_python()` in a `Field` subclass to change conversion, and `validate()` for type-level rules; call `super()` in `validate()` to keep the `required` check. - Leave `run_validators()` alone; add callables to `validators=` instead. - Leave `full_clean()` alone; the per-field and form-wide hooks exist so you never need to. - In `Form.clean()`, call `super().clean()` first so rules defined by parent forms and mixins still run. - Keep hooks free of side effects such as sending e-mail or writing rows: `full_clean()` can run on any bound form, including ones that end up invalid. ## Pitfalls interviewers probe - A `clean_<fieldname>()` without `return`. - Cross-field checks placed in `clean_<fieldname>()`, which break when field order changes. - `self.cleaned_data['x']` in `clean()` raising `KeyError` after `x` failed. - Expecting validators to run on an empty optional field.
- In a Django form, why does clean_<fieldname>() not run for a field that failed its type conversion?Django calls the per-field method only after `Field.clean()` returns successfully, because it is written against the cleaned Python value in `cleaned_data`. If `to_python()`, `validate()` or a validator raised, there is no clean value to refine, so the error is recorded and Django moves on to the next field.
- In Django, what happens if a form's clean() method returns None instead of cleaned_data?Nothing breaks: Django only replaces `cleaned_data` when `clean()` returns a value that is not `None`. Returning a different dictionary does replace it, which is occasionally used to add derived keys, but mutating `self.cleaned_data` and returning it is the clearer convention.
- Does calling form.errors before is_valid() in Django change validation behaviour?It triggers the same `full_clean()` that `is_valid()` would, once. Later calls reuse the stored result, so validation does not run twice; `is_valid()` then just checks that the form is bound and the error dictionary is empty.
saying these in an interview costs you the question
- clean() is skipped whenever any field already has an error.
- is_valid() stops at the first field that fails.
- clean_<field>() runs before the field's own type conversion.
- A clean_<field>() method does not need to return anything.
- Validators run before to_python(), on the raw string.