14.3 Validation: full_clean, cleaned_data and the errors
BaseForm validates a bound form: it cleans each field, runs the form’s clean, and leaves two results, cleaned_data for the values that passed and an ErrorDict of ErrorList objects for the rest, every error filed by BaseForm. BaseForm.errors and BaseForm run it the first time they are asked.
A view asks a form whether its data will do, and then wants the values or the errors, each filed under its input. A program tests an error’s code, "required" or "min_value", and a page shows its message, so a form keeps each error as a ValidationError, which has both, in a list that draws itself as HTML.
When validation runs: errors and is_valid
Reading BaseForm.errors while the form’s _errors is None, as BaseForm leaves it, calls full_clean; after that it returns what was found. BaseForm is the form being bound and its errors empty. So an unbound form is never valid, and its validation finds no errors and makes no cleaned_data. A call of full_clean validates the form afresh.
BaseForm.full_clean
BaseForm first sets _errors to a new ErrorDict, made with the form’s renderer, and only then looks at the data; whatever files an error during the validation reads errors, and finds that dictionary. An unbound form stops there.
A bound form is given an empty cleaned_data, and then one short cut: a form made with empty_permitted=True whose data has not changed from its initial values stops at once, valid, with nothing in cleaned_data and none of its fields cleaned, not even the required ones. A formset makes its extra forms past its minimum so, and a blank one is no error (Formsets: BaseFormSet and the management form). Otherwise three steps run, each whatever the one before found: _clean_fields, _clean_form and _post_clean.
Each field in turn: _clean_fields
BaseForm takes the fields in the order of the form’s fields. For each it puts in cleaned_data the value Field cleans: what the bound field read from the data, or the initial value for a disabled field (A form field: the base class).
Then, if the form has a method whose name is "clean_" and the field’s name, it is called with no arguments. It finds the field’s value in cleaned_data, where BerthForm’s method for the ship reads it, and whatever it returns takes the value’s place: a method that ends without a return makes the value None, and the form is still valid. Since the fields go one at a time, such a method sees the fields before its own that passed, and none after: a method for the arrival date could read the ship’s name, and the ship’s method cannot read the date. A check of two fields waits for the form’s clean.
A ValidationError from the field or the method goes to add_error under the field’s name, and the field is left out of cleaned_data: a value the field had cleaned before its method failed is taken out again. A field that fails never reaches its method.
The whole form: clean, _clean_form and _post_clean
BaseForm calls BaseForm.clean once every field has been cleaned, whether or not any failed, so a clean finds in cleaned_data only the fields that passed. In BaseForm it returns cleaned_data.
A None from clean, unlike one from a field’s method, leaves cleaned_data as it is; anything else replaces it whole. A ValidationError it raises goes to add_error with no field, which files an error made from a message or a list under "__all__", the constant NON_FIELD_ERRORS, and one made from a dictionary under its keys. On a model form the inherited BaseModelForm switches on the unique checks and the constraints, and an override that does not call it goes without them (Model forms: ModelForm and fields_for_model).
A form for a stay adds a departure and a check of the two dates:
# harbour/fleet/forms.py
class StayForm(BerthForm):
departs = forms.DateField()
def clean(self):
cleaned_data = super().clean()
arrives, departs = cleaned_data.get("arrives"), cleaned_data.get("departs")
if arrives and departs and departs < arrives:
self.add_error("departs", "A ship cannot leave before it arrives.")
>>> form = StayForm({"ship": "heron", "tonnage": "0", "arrives": "2026-10-20", "departs": "2026-10-18"})
>>> form.is_valid()
False
>>> form.errors.as_data()
{'tonnage': [ValidationError(['Ensure this value is greater than or equal to 1.'])], 'departs': [ValidationError(['A ship cannot leave before it arrives.'])]}
>>> form.cleaned_data
{'ship': 'Heron', 'arrives': datetime.date(2026, 10, 20)}
The tonnage failed in its field and the departure in the form’s check. Both are filed by field, in the order they were found, and neither is left in cleaned_data, which clean, returning None, kept.
Last, whatever was found, runs BaseForm: a hook that does nothing in BaseForm and in which a model form fills its instance and has it validated.
BaseForm.add_error
BaseForm files every error a form’s validation finds, and a clean or a view may call it too; a model formset writes its unique checks’ errors into its forms directly (Model formsets and inline formsets). It takes a field’s name or None, and an error: a ValidationError, or a message, a list or a dictionary of names, which it makes one (Validating an instance).
An error made from a dictionary carries its own names, and the field must be None: given a name too, the method raises TypeError. Any other error is filed under the name given, or "__all__" for None or an empty name. Then, for each name:
- A name that is neither
"__all__"nor a field of the form raisesValueError. - The first error under a name makes its list, of the form’s
error_class: for"__all__"with the extra class"nonfield", and for a field withfield_id, the field’sBoundField..auto_id - The errors are added to the list, as
ValidationErrorobjects. - The name is taken out of
cleaned_data, if it is there.
add_error reads errors, so a call before the validation runs it first, and a call after it, from a view, leaves the form not valid. On an unbound form, which has no cleaned_data, the call files the error and then fails with AttributeError.
BaseForm says whether a field, or "__all__", has an error, and, given a code, whether one of its errors has that code. BaseForm returns the list under "__all__", or an empty list of the class "nonfield". A bound field’s errors is the list under its name, or an empty one (BoundField: a field in a form).
has_changed and changed_data
BaseForm lists the fields whose data differs from their initial values, and BaseForm says whether it lists any. Each bound field answers by Field, given its data and its initial value, which a field with show_hidden_initial reads from a hidden input in the data. changed_data is a cached property, worked out once.
Validation reads it for the short cut of empty_permitted. A formset counts an extra form that has not changed as empty (Formsets: BaseFormSet and the management form). A model formset saves the forms that have changed and records each changed object with its changed_data (Model formsets and inline formsets).
ErrorDict and ErrorList
A form’s errors are an ErrorDict, a dict from names to lists that keeps a renderer, and each list is of the form’s error_class, an ErrorList unless the form was given another. The list keeps in data the ValidationError objects that add_error gives it, or plain strings, and shows messages: indexing gives an error’s message with its parameters put in, and a string as it is; since iteration, repr, in and == all go through indexing, a list of errors prints and compares as a list of strings. ErrorList gives each entry as a ValidationError. The class derives from list as well as collections only so that isinstance takes it for a list, for backward compatibility, a comment says, citing ticket 17413.
Both answer the same methods, as_json, as_text, as_ul and str from RenderableErrorMixin and its base:
django/forms/templates/django/forms/errors/ .| method | an ErrorList |
an ErrorDict |
|---|---|---|
as_data |
an ErrorList its entries as ValidationError objects |
an ErrorDict each name’s as_data |
get_json_data |
an ErrorList a message and a code, or "", for each error |
an ErrorDict each name’s get_json_data |
as_json |
an ErrorList get_json_data as JSON |
an ErrorDict the same |
as_text |
an ErrorList a line for each message |
an ErrorDict each name, its messages under it |
as_ul, str |
an ErrorList a <ul> of the messages |
an ErrorDict a <ul> with each name and its list |
A list’s error_class is the CSS class of its <ul>: "errorlist", then any it was made with, "nonfield" for a form’s own errors. Its field_id gives the <ul> an id, "id_departs_error" for StayForm’s departure, which the input’s "aria-describedby" names (BoundField: a field in a form). ErrorList.copy keeps the class and the renderer; BaseForm adds to a copy of the non-field errors each hidden field’s errors, the field’s name in front (Rendering a form: renderers and templates).
ErrorList, which pickling and deep copying call, drops the iterator a list adds to its reduction for filling the copy again, which the same comment says would give duplicate entries, citing ticket 23594.