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.
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 fillsform.cleaned_dataorform.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
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
ifstatements spread around; keep rules in forms or serializers. - ModelForms with
fields = "__all__", exposing fields such asis_staffadded to the model later. - Disabling CSRF protection on HTML forms to "fix" 403 errors.
Key points
- Forms validate and convert input;
cleaned_dataholds 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.