Q&A
How to do Django form validation
Overview
Django validates forms in a specific sequence: field-level cleaning (type conversion, built-in constraints) → custom clean_<field>() → form-level clean(). Each stage can raise ValidationError to add an error. This layered approach keeps simple rules close to the field and complex, cross-field logic in one place.
graph TD
A["form.is_valid() called"] --> B["Field.clean(): type coerce + built-in validators"]
B --> C["clean_fieldname(): custom per-field logic"]
C --> D["form.clean(): cross-field validation"]
D --> E{Any errors?}
E -->|yes| F["form.errors populated; is_valid() returns False"]
E -->|no| G["form.cleaned_data ready; is_valid() returns True"]
style D fill:#1e1b4b,color:#fff
A Simple Form
from django import forms
class RegistrationForm(forms.Form):
username = forms.CharField(max_length=50)
email = forms.EmailField()
password = forms.CharField(min_length=8, widget=forms.PasswordInput)
confirm = forms.CharField(widget=forms.PasswordInput)
CharField, EmailField, etc. handle type coercion and basic validation automatically. A missing required field, an invalid email, or a password shorter than 8 chars all fail at this level.
Using in a View
def register(request):
if request.method == "POST":
form = RegistrationForm(request.POST)
if form.is_valid():
# All validation passed; cleaned_data is available
username = form.cleaned_data["username"]
create_user(username, form.cleaned_data["email"], form.cleaned_data["password"])
return redirect("home")
else:
form = RegistrationForm()
return render(request, "register.html", {"form": form})
form.cleaned_data contains the validated, type-converted values only after is_valid() returns True.
Custom Per-Field Validation: clean_fieldname()
Add a clean_<fieldname>() method to apply extra logic to one field:
class RegistrationForm(forms.Form):
username = forms.CharField(max_length=50)
# ...
def clean_username(self):
username = self.cleaned_data["username"]
if User.objects.filter(username=username).exists():
raise forms.ValidationError("Username already taken.")
return username # MUST return the value
If you raise ValidationError, the error appears under that field. If you don't, always return the (possibly transformed) value.
Cross-Field Validation: clean()
For rules that span multiple fields (e.g. "passwords must match"):
def clean(self):
cleaned = super().clean() # run default clean first
password = cleaned.get("password")
confirm = cleaned.get("confirm")
if password and confirm and password != confirm:
raise forms.ValidationError("Passwords do not match.")
return cleaned
Raise ValidationError from clean() for non-field (form-level) errors, or use self.add_error("field_name", "message") to attach the error to a specific field even from clean().
Validators: Reusable Validation Functions
For reusable constraints, write a validator function and attach it to the field:
from django.core.exceptions import ValidationError
def no_spaces(value):
if " " in value:
raise ValidationError("Spaces are not allowed.")
class SearchForm(forms.Form):
query = forms.CharField(validators=[no_spaces])
Django also provides many built-in validators: MaxLengthValidator, RegexValidator, URLValidator, EmailValidator, etc.
ModelForm: Validation + Model Constraints
ModelForm automatically creates fields and validators from the model definition:
class ProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ["bio", "website"]
def clean_website(self):
url = self.cleaned_data.get("website", "")
if url and not url.startswith("https://"):
raise forms.ValidationError("Only HTTPS URLs allowed.")
return url
ModelForm.save() calls the model's full_clean() before saving, adding the model-level uniqueness and constraint checks.
Displaying Errors in Templates
<form method="post">
{% csrf_token %}
{{ form.non_field_errors }} {# form-level errors from clean() #}
{% for field in form %}
<label>{{ field.label }}</label>
{{ field }}
{{ field.errors }} {# field-level errors #}
{% endfor %}
<button type="submit">Submit</button>
</form>
Why This Matters in Interviews
Django interviews use form validation to test understanding of the request/response cycle and data integrity. Cover: the three-stage pipeline (field → clean_field() → clean()), that cleaned_data is only available after is_valid(), that clean_<field>() must return the value, and clean() for cross-field rules (attach with add_error for field-specific errors from clean()). ModelForm automatically deriving validators from the model, and writing reusable validators, rounds out the answer.
Common Pitfalls
- Forgetting to
return cleaned_data["field"]at the end ofclean_fieldname()- the field getsNone. - Calling
clean()withoutsuper().clean()- built-in cross-field validation is skipped. - Raising
ValidationErrorinclean()but wanting it on a specific field - useself.add_error("field", "message"). - Accessing
cleaned_databefore callingis_valid()- it doesn't exist yet. - Not rendering
{{ form.non_field_errors }}in the template - form-level errors become invisible to the user.
References
- Django Form validation: https://docs.djangoproject.com/en/stable/ref/forms/validation/
- Django built-in validators: https://docs.djangoproject.com/en/stable/ref/validators/