When I first started learning Django, forms looked magical. I would write a few lines, call is_valid(), and Django somehow knew whether my data was correct or not.
Only later did I realize how much work Django does behind the scenes (batteries included) type conversion, sanitization, security checks, and structured validation.
Forms are the front gate of your application. Every piece of user input—login pages, registrations, payment details, profile updates—flows through Django forms. If this layer is weak, your entire application becomes fragile.
In this article, we will slowly walk from
simple forms → custom validations → advanced ModelForm workflows,
and also answer the most confusing interview questions
- What exactly is
cleaned_data? - Difference between
clean()andclean_<field>() - How does ModelForm validation actually work?
- How to validate multiple fields together?
- How to validate file size and type?
Understanding the Validation Pipeline
Whenever a user submits a form, Django does not trust the raw data. The request.POST dictionary is just strings coming from the browser. Django follows a strict pipeline before allowing you to use that data,
- It reads the raw input
- Converts fields into proper Python types
- Runs built-in validators
- Executes your custom validation methods
- Stores the final safe result in
cleaned_data
Only after all these steps does form.is_valid() return True. This design is what makes Django forms so reliable.
1. Simple Form with Built-in Validation
At the beginner level, Django already gives us a lot. Fields like EmailField, IntegerField, min_length, required=True work out of the box without writing a single line of custom logic.
class SimpleRegisterForm(forms.Form): username = forms.CharField(max_length=20, min_length=4, required=True) email = forms.EmailField(required=True) password = forms.CharField(widget=forms.PasswordInput, min_length=6)
from django.shortcuts import renderfrom .forms import SimpleRegisterFormdef simple_view(request): form = SimpleRegisterForm() if request.method == "POST": form = SimpleRegisterForm(request.POST) if form.is_valid(): # cleaned_data is safe to use print(form.cleaned_data) return render(request, "success.html") return render(request, "simple.html", {"form": form})
<form method="POST"> {% csrf_token %} {{ form.as_p }} <button type="submit">Submit</button></form>
Here we did not write any validation code, but Django will automatically
- ensure username length is between 4 and 20
- check email format
- enforce password minimum length
- prevent empty submissions
This is why Django forms feel “batteries included”. For many internal tools, this level itself is enough.
2. Custom Field Validations
Real applications always have business rules. Maybe you don’t want users with certain words in username, or you want password complexity rules, or you need to validate an Indian phone number.
Django gives a beautiful hook called
clean_<field>()
This method runs after built-in validation but only for that specific field.
from django import formsimport reclass ProfileForm(forms.Form): username = forms.CharField(max_length=20) password = forms.CharField(widget=forms.PasswordInput) phone = forms.CharField(max_length=10) def clean_username(self): username = self.cleaned_data.get("username") if "admin" in username.lower(): raise forms.ValidationError("Username cannot contain 'admin'") return username def clean_password(self): password = self.cleaned_data.get("password") if not any(char.isdigit() for char in password): raise forms.ValidationError("Password must contain a number") return password def clean_phone(self): phone = self.cleaned_data.get("phone") if not re.match(r"^[6-9]\d{9}$", phone): raise forms.ValidationError("Invalid Indian phone number") return phone
The important idea here is,
- You receive already parsed value
- You can write any Python logic
- You must return the value
- Raise ValidationError if invalid
This keeps validation logic very close to the form instead of scattering it in views.
3. Advanced ModelForm Validation
When your form is connected to a database model, you usually use ModelForm. It combines
- model constraints
- form level checks
- custom business rules
- save logic
Imagine a Student registration
- age must be 18+
- password and confirm password must match
- profile image must be < 2MB
- only JPG/PNG allowed
This requires both field-level and form-level validation.
from django.db import modelsclass Student(models.Model): name = models.CharField(max_length=50) email = models.EmailField(unique=True) age = models.IntegerField() password = models.CharField(max_length=100) profile_pic = models.ImageField(upload_to="profiles/")
from django import formsfrom .models import Studentclass StudentForm(forms.ModelForm): confirm_password = forms.CharField(widget=forms.PasswordInput) class Meta: model = Student fields = ["name", "email", "age", "password", "profile_pic"] widgets = { "password": forms.PasswordInput, "age": forms.NumberInput(attrs={"min": 18}), } def clean(self): cleaned_data = super().clean() password = cleaned_data.get("password") confirm = cleaned_data.get("confirm_password") if password != confirm: raise forms.ValidationError("Passwords do not match") return cleaned_data def clean_profile_pic(self): image = self.cleaned_data.get("profile_pic") if image.size > 2 * 1024 * 1024: raise forms.ValidationError("Image size must be < 2MB") return image
from django.shortcuts import renderfrom .forms import StudentFormdef student_view(request): if request.method == "POST": form = StudentForm(request.POST, request.FILES) if form.is_valid(): form.save() return render(request, "success.html") else: form = StudentForm() return render(request, "student.html", {"form": form})
Here the form already knows
- email must be unique (from model)
- field types
- required constraints
On top of that we add our logic using clean() and clean_profile_pic().
Django forms are more than HTML generators. They are a full validation framework sitting between the user and your database. Once you respect this layer, your application automatically becomes
- more secure
- more maintainable
- less buggy