Difficulty: Intermediate
Django's form validation runs in a specific order when you call form.is_valid(). Understanding this order is crucial for implementing custom validation correctly.
The validation process has three phases. First, each field's built-in validation runs (checking required, data type, max_length, etc.) and the field's to_python() method converts the raw string to a Python type. Second, each field's custom clean_fieldname() method runs, allowing field-specific validation. Third, the form's clean() method runs, enabling cross-field validation.
The clean_fieldname() method validates a single field. It must return the cleaned value (which can be modified) or raise a ValidationError. This method has access to self.cleaned_data, but only fields that have already passed their built-in validation are available. A common use case is checking uniqueness, format requirements, or business rules for individual fields.
The clean() method validates across multiple fields. It runs after all individual field validations complete. Use it when validation depends on the relationship between two or more fields - for example, ensuring a password confirmation matches the password, or that an end date is after a start date. It should return self.cleaned_data (the complete dictionary) or raise a ValidationError.
ValidationError can be raised with a single message string or a list of messages. For form-level errors (from clean()), the errors appear in form.non_field_errors. For field-level errors (from clean_fieldname()), they appear on the specific field. You can also use self.add_error('fieldname', 'message') to add errors to specific fields from within clean().
Django also supports validators - reusable functions or classes that can be applied to model fields and form fields. Built-in validators include MinValueValidator, MaxValueValidator, MinLengthValidator, MaxLengthValidator, RegexValidator, EmailValidator, and URLValidator. You can create custom validators as functions that raise ValidationError on invalid input.
Model-level validation through the model's clean() method and field validators is automatically included in ModelForms. This means validation logic defined on the model is reused in forms without duplication.
from django import forms
from django.core.exceptions import ValidationError
class RegistrationForm(forms.Form):
username = forms.CharField(max_length=30)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
def clean_username(self):
username = self.cleaned_data['username']
if username.lower() in ['admin', 'root', 'superuser']:
raise ValidationError('This username is reserved.')
if not username.isalnum():
raise ValidationError('Username must be alphanumeric.')
return username.lower() # Return normalized value
def clean_email(self):
email = self.cleaned_data['email']
if email.endswith('@example.com'):
raise ValidationError('Example.com emails are not allowed.')
return email
clean_fieldname() methods validate individual fields. They access the field value from self.cleaned_data, perform validation, and return the (potentially modified) value. Raise ValidationError to mark the field as invalid.
from django import forms
from django.core.exceptions import ValidationError
class RegistrationForm(forms.Form):
password = forms.CharField(widget=forms.PasswordInput, min_length=8)
confirm_password = forms.CharField(widget=forms.PasswordInput)
start_date = forms.DateField()
end_date = forms.DateField()
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
confirm = cleaned_data.get('confirm_password')
if password and confirm and password != confirm:
raise ValidationError('Passwords do not match.')
# OR: attach error to a specific field
# self.add_error('confirm_password', 'Passwords do not match.')
start = cleaned_data.get('start_date')
end = cleaned_data.get('end_date')
if start and end and end <= start:
self.add_error('end_date', 'End date must be after start date.')
return cleaned_data
The clean() method validates across fields. Use super().clean() first. Access fields with .get() since they may not exist if their individual validation failed. Use add_error() to attach errors to specific fields, or raise ValidationError for non-field errors.
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
import re
# Function-based validator
def validate_no_profanity(value):
bad_words = ['spam', 'scam']
for word in bad_words:
if word in value.lower():
raise ValidationError(
f'Content contains prohibited word: {word}'
)
# Regex validator
phone_validator = RegexValidator(
regex=r'^\+?1?\d{9,15}#39;,
message='Enter a valid phone number (9-15 digits).',
)
# Using validators on form fields
class ProfileForm(forms.Form):
bio = forms.CharField(
widget=forms.Textarea,
validators=[validate_no_profanity],
)
phone = forms.CharField(
max_length=16,
validators=[phone_validator],
)
# Using validators on model fields
class Profile(models.Model):
bio = models.TextField(validators=[validate_no_profanity])
phone = models.CharField(max_length=16, validators=[phone_validator])
Validators are reusable validation functions. They take a value and raise ValidationError if invalid. They can be used on both form fields and model fields. RegexValidator validates against a regular expression pattern.
# Django validation order when form.is_valid() is called:
#
# 1. For each field (in definition order):
# a. field.to_python() - Convert raw value to Python type
# b. field.validate() - Run built-in field validation
# c. field.run_validators() - Run field-level validators
# d. form.clean_fieldname() - Run custom field validation
#
# 2. form.clean() - Run cross-field validation
#
# 3. If any step raises ValidationError:
# - is_valid() returns False
# - Errors are stored in form.errors
# - form.cleaned_data only has fields that passed validation
# Accessing errors in templates:
# {{ form.errors }} - All errors as dict
# {{ form.non_field_errors }} - Errors from clean()
# {{ form.username.errors }} - Errors for specific field
Understanding validation order prevents bugs. Field-level validation runs before form-level validation. If a field fails its own validation, it will not be in cleaned_data when clean() runs, so always use .get() in clean().
clean, clean_fieldname, ValidationError, Validators, Cross-field Validation