Skip to main content

Forms and Validation

The forms framework in this codebase provides a declarative way to define HTML forms, handle data validation, and convert user input into Python objects. It is built around two primary classes: Form for general-purpose input and ModelForm for forms directly tied to database models.

Core Form Classes

The framework distinguishes between the logic of a form and its declaration:

  • BaseForm: Located in django/forms/forms.py, this class contains the core logic for validation, data cleaning, and rendering. It does not define any fields itself.
  • Form: Inherits from BaseForm and uses DeclarativeFieldsMetaclass. This allows developers to define fields as class attributes, which the metaclass then collects into a fields dictionary.
  • ModelForm: Found in django/forms/models.py, this class automatically generates form fields based on a Django Model's definition.

The Validation Lifecycle

Validation is triggered by calling is_valid() on a bound form instance. This method returns a boolean and populates the errors and cleaned_data attributes.

The internal flow of validation follows these steps:

  1. is_valid(): Checks if the form is bound (has data) and if errors is empty.
  2. full_clean(): The entry point for the cleaning process. It initializes the errors dictionary and calls three internal cleaning methods:
    • _clean_fields(): Iterates through every field in the form. It calls the field's own clean() method and then looks for a method on the form named clean_<fieldname>() for custom field-specific validation.
    • _clean_form(): Calls the form's clean() method, which is used for cross-field validation.
    • _post_clean(): An internal hook (primarily used by ModelForm) to perform final validation, such as model-level uniqueness checks.

Field-Level Validation

Individual fields (subclasses of Field in django/forms/fields.py) handle their own validation through the clean() method. This method executes three distinct steps:

# django/forms/fields.py

def clean(self, value):
"""
Validate the given value and return its "cleaned" value as an
appropriate Python object. Raise ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
  • to_python(): Coerces the raw input (usually a string) into a Python object (e.g., an integer or a datetime object).
  • validate(): Performs basic validation, such as checking if a required field is empty.
  • run_validators(): Executes a list of validator functions (e.g., EmailValidator) against the value.

Custom Validation Patterns

The framework provides two primary hooks for adding custom validation logic to a form.

Cross-Field Validation

To validate multiple fields together, override the clean() method. A classic example is the AuthenticationForm in django/contrib/auth/forms.py, which validates the username and password combination:

# django/contrib/auth/forms.py

class AuthenticationForm(forms.Form):
# ... fields defined here ...

def clean(self):
username = self.cleaned_data.get("username")
password = self.cleaned_data.get("password")

if username is not None and password:
self.user_cache = authenticate(self.request, username=username, password=password)
if self.user_cache is None:
raise self.get_invalid_login_error()
return self.cleaned_data

Field-Specific Validation

To add validation to a single field, define a method named clean_<fieldname>(). This method is called after the field's own clean() method has succeeded. It should return the cleaned value.

ModelForms

ModelForm simplifies creating forms that interact with database models. It uses a Meta inner class to define which model to use and which fields to include.

The BaseUserCreationForm demonstrates how ModelForm is used to create a user while adding extra fields (like password confirmation) that are not part of the model:

# django/contrib/auth/forms.py

class BaseUserCreationForm(SetPasswordMixin, forms.ModelForm):
password1, password2 = SetPasswordMixin.create_password_fields()

class Meta:
model = User
fields = ("username",)
field_classes = {"username": UsernameField}

def save(self, commit=True):
user = super().save(commit=False)
user = self.set_password_and_save(user, commit=commit)
return user

When save() is called on a ModelForm, it creates or updates the model instance. If commit=False is passed, it returns the instance without saving it to the database, allowing for further modification.

Formsets

Formsets (defined in django/forms/formsets.py) manage multiple instances of the same form on a single page. They rely on a ManagementForm to track the number of forms submitted.

The ManagementForm contains hidden fields that the BaseFormSet uses to determine how many forms to process:

  • TOTAL_FORMS: The total number of forms sent.
  • INITIAL_FORMS: The number of forms that were initially loaded (used to detect changes).

If the ManagementForm data is missing or tampered with, the formset will raise a ValidationError during initialization to prevent data corruption or security issues.

Rendering and BoundFields

When a form is accessed in a template, it uses BoundField (from django/forms/boundfield.py) to wrap each Field with its associated data and errors.

The rendering process is handled by a renderer (configured via FORM_RENDERER). Forms can be rendered as a whole using methods like as_p(), as_table(), or as_div(), which utilize templates found in django/forms/templates/django/forms/. Individual fields can also be rendered manually by iterating over the form instance, which yields BoundField objects.