AlgoInsight

Q&A

How to do Django form validation

By AlgoInsight Team2026-06-18djangopython

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

References

More answers