Django from Basics to Production · Django Fundamentals

Forms and Validation

Validate user input with Django forms: field validation, custom clean methods, cross-field checks and error messages.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A signup form with typed fields, a custom field check (no disposable email domains) and a cross-field check (passwords match), used from a view that returns errors or a success response.

The idea

  • A Form declares fields with types and constraints; form.is_valid() runs validation and fills form.cleaned_data or form.errors.
  • clean_<field>() methods validate one field; clean() validates combinations of fields.
  • ModelForms generate fields from a model; use fields = [...] explicitly so new model fields aren't exposed by accident.
  • Forms render HTML with CSRF protection in templates; for JSON APIs, Django REST framework serializers play the same role.

The code

A signup form with custom and cross-field validation
import sys, types
import django
from django.conf import settings

sys.modules["shop"] = types.ModuleType("shop")       # lets this single file act as the "shop" app
sys.modules["shop"].__file__ = __file__
settings.configure(
    SECRET_KEY="dev-only", DEBUG=False, ALLOWED_HOSTS=["testserver"], ROOT_URLCONF=__name__,
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
    INSTALLED_APPS=["django.contrib.contenttypes", "django.contrib.auth", "shop"],
)
django.setup()

from django import forms
from django.http import JsonResponse
from django.urls import path

class SignupForm(forms.Form):
    email = forms.EmailField()
    age = forms.IntegerField(min_value=16)
    password = forms.CharField(min_length=10)
    password_again = forms.CharField()

    def clean_email(self):
        email = self.cleaned_data["email"].lower()
        if email.endswith("@mailinator.com"):
            raise forms.ValidationError("Please use a permanent email address.")
        return email

    def clean(self):
        data = super().clean()
        if data.get("password") and data.get("password") != data.get("password_again"):
            self.add_error("password_again", "Passwords don't match.")
        return data

def signup(request):
    form = SignupForm(request.POST)
    if not form.is_valid():
        return JsonResponse({"errors": {k: [str(m) for m in v] for k, v in form.errors.items()}}, status=400)
    return JsonResponse({"welcome": form.cleaned_data["email"]}, status=201)

urlpatterns = [path("signup/", signup)]

from django.test import Client
c = Client()
for data in [{"email": "[email protected]", "age": "30", "password": "long-enough-pw", "password_again": "long-enough-pw"},
             {"email": "[email protected]", "age": "12", "password": "short", "password_again": "other"}]:
    r = c.post("/signup/", data)
    print(r.status_code, r.json())

Output

201 {'welcome': '[email protected]'}
400 {'errors': {'email': ['Please use a permanent email address.'], 'age': ['Ensure this value is greater than or equal to 16.'], 'password': ['Ensure this value has at least 10 characters (it has 5).']}}

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Validating in views with if statements spread around; keep rules in forms or serializers.
  • ModelForms with fields = "__all__", exposing fields such as is_staff added to the model later.
  • Disabling CSRF protection on HTML forms to "fix" 403 errors.

Key points

  • Forms validate and convert input; cleaned_data holds the result.
  • clean_<field> for single fields, clean() for cross-field rules.
  • List fields explicitly in ModelForms.

Further reading: Working with forms · Form and field validation

Practice questions

Where do you put a validation rule that involves two fields?

Answer

In the form's clean() method, using add_error() to attach the message to the right field.

Why avoid fields = '__all__' in ModelForms?

Answer

Fields added to the model later (for example is_staff) become editable by users automatically; listing fields explicitly prevents mass assignment.

What does form.cleaned_data contain?

Answer

The validated and converted values (for example an int for IntegerField, a normalised email), available after is_valid() returns True.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?