Django Form Validation 🛡️
The Security Guard at Your Data’s Front Door
The Big Picture: What is Form Validation?
Imagine you’re hosting a fancy birthday party. You have a guest list. At the door stands a security guard (that’s Django’s form validation!).
The guard checks:
- Is this person’s name on the list? ✅
- Are they wearing proper clothes? ✅
- Did they bring a gift (not a water balloon)? ✅
Form validation is Django’s security guard - it checks every piece of data before letting it into your application.
graph TD A["User Submits Form"] --> B["Django Receives Data"] B --> C{Validation Process} C -->|Pass| D["✅ Data is Clean & Safe"] C -->|Fail| E["❌ Show Errors to User"] D --> F["Save to Database"] E --> G["User Fixes & Resubmits"]
1. The Form Validation Process 🔄
How It Works - Step by Step
Think of validation like washing vegetables before cooking:
- Receive - You get dirty vegetables (raw user data)
- Check - Look for bugs, dirt, bad spots (validation)
- Clean - Wash and prepare (data cleaning)
- Use - Now safe to cook! (save to database)
The Magic Method: is_valid()
# In your view
def signup(request):
if request.method == 'POST':
form = SignupForm(request.POST)
# This ONE line triggers ALL validation!
if form.is_valid():
# Data is clean and safe
form.save()
else:
# Errors exist - show them
print(form.errors)
What happens inside is_valid()?
- Runs
to_python()- converts data to Python types - Runs
validate()- checks basic rules - Runs
run_validators()- runs custom validators - Runs
clean_<fieldname>()- field-level cleaning - Runs
clean()- form-level cleaning
2. cleaned_data and Form Errors 📦
cleaned_data - Your Gift Box of Clean Data
After validation passes, Django gives you a special dictionary called cleaned_data. It’s like getting your clothes back from the dry cleaner - clean, pressed, and ready!
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
age = forms.IntegerField()
# In your view
if form.is_valid():
# Access the CLEAN data
name = form.cleaned_data['name']
email = form.cleaned_data['email']
age = form.cleaned_data['age'] # This is now an INTEGER, not a string!
Why use cleaned_data?
- Data types are correct (strings become integers, dates become date objects)
- Extra whitespace is trimmed
- Data is validated and safe
Form Errors - The Problem Report
When validation fails, Django collects all problems in form.errors:
if not form.is_valid():
# Dictionary of field -> error messages
print(form.errors)
# Output: {'email': ['Enter a valid email address.']}
# Get errors for specific field
print(form.errors.get('email'))
# Get ALL errors as simple list
print(form.errors.as_text())
In your template:
<form method="post">
{% csrf_token %}
<!-- Show all errors at top -->
{% if form.errors %}
<div class="alert alert-danger">
Please fix the errors below.
</div>
{% endif %}
<!-- Show field with its error -->
<div>
{{ form.email }}
{% if form.email.errors %}
<span class="error">
{{ form.email.errors.0 }}
</span>
{% endif %}
</div>
<button type="submit">Submit</button>
</form>
3. Field-Level Validation 🎯
Check ONE Field at a Time
Field-level validation is like checking one ingredient before adding it to your recipe. Is this tomato fresh? Is this egg not cracked?
Use clean_<fieldname>() method:
class SignupForm(forms.Form):
username = forms.CharField(max_length=50)
email = forms.EmailField()
age = forms.IntegerField()
def clean_username(self):
"""Check username is not taken"""
username = self.cleaned_data['username']
# Check if username exists
if User.objects.filter(
username=username
).exists():
raise forms.ValidationError(
"This username is already taken!"
)
# Always return the cleaned value
return username.lower() # Also make it lowercase
def clean_age(self):
"""Check age is at least 13"""
age = self.cleaned_data['age']
if age < 13:
raise forms.ValidationError(
"You must be at least 13 years old."
)
return age
Key Rules:
- Method name must be
clean_+ field name - Access the value from
self.cleaned_data - Raise
ValidationErrorif something’s wrong - Always return the value (even if unchanged)
4. Form-Level Validation 🔗
Check Multiple Fields Together
Sometimes you need to check how fields relate to each other. Like making sure your password and confirm password match!
Use the clean() method:
class PasswordChangeForm(forms.Form):
old_password = forms.CharField(
widget=forms.PasswordInput
)
new_password = forms.CharField(
widget=forms.PasswordInput
)
confirm_password = forms.CharField(
widget=forms.PasswordInput
)
def clean(self):
"""Check passwords match"""
# Get ALL cleaned data
cleaned_data = super().clean()
new_pw = cleaned_data.get('new_password')
confirm_pw = cleaned_data.get('confirm_password')
# Compare two fields
if new_pw and confirm_pw:
if new_pw != confirm_pw:
raise forms.ValidationError(
"Passwords don't match!"
)
return cleaned_data
Another Example - Date Range:
class BookingForm(forms.Form):
check_in = forms.DateField()
check_out = forms.DateField()
def clean(self):
cleaned_data = super().clean()
check_in = cleaned_data.get('check_in')
check_out = cleaned_data.get('check_out')
if check_in and check_out:
if check_out <= check_in:
raise forms.ValidationError(
"Check-out must be after check-in!"
)
return cleaned_data
5. Custom Validators ⚡
Reusable Validation Functions
What if you want to use the same check in many forms? Create a custom validator - a reusable function!
Simple Function Validator:
from django.core.exceptions import ValidationError
def validate_no_bad_words(value):
"""Check for inappropriate content"""
bad_words = ['spam', 'scam', 'fake']
for word in bad_words:
if word in value.lower():
raise ValidationError(
f"'{word}' is not allowed!"
)
# Use it in your form
class CommentForm(forms.Form):
comment = forms.CharField(
validators=[validate_no_bad_words]
)
Class-Based Validator (with parameters):
from django.core.exceptions import ValidationError
class MinWordsValidator:
"""Ensure text has minimum word count"""
def __init__(self, min_words=10):
self.min_words = min_words
def __call__(self, value):
word_count = len(value.split())
if word_count < self.min_words:
raise ValidationError(
f"Please write at least "
f"{self.min_words} words. "
f"You wrote {word_count}."
)
# Use it
class EssayForm(forms.Form):
essay = forms.CharField(
widget=forms.Textarea,
validators=[MinWordsValidator(50)]
)
summary = forms.CharField(
validators=[MinWordsValidator(10)]
)
6. Built-in Validators 🧰
Django’s Ready-Made Validators
Django comes with many validators already built! No need to reinvent the wheel.
from django.core.validators import (
MinValueValidator,
MaxValueValidator,
MinLengthValidator,
MaxLengthValidator,
RegexValidator,
EmailValidator,
URLValidator,
)
class ProductForm(forms.Form):
# Number range
price = forms.DecimalField(
validators=[
MinValueValidator(0.01),
MaxValueValidator(99999.99)
]
)
# Text length
description = forms.CharField(
validators=[
MinLengthValidator(10),
MaxLengthValidator(500)
]
)
# Pattern matching
product_code = forms.CharField(
validators=[
RegexValidator(
regex=r'^[A-Z]{2}\d{4}#x27;,
message="Code format: AB1234"
)
]
)
# URL validation
website = forms.CharField(
validators=[URLValidator()]
)
Common Built-in Validators
| Validator | What It Checks |
|---|---|
MinValueValidator(n) |
Number >= n |
MaxValueValidator(n) |
Number <= n |
MinLengthValidator(n) |
Text length >= n |
MaxLengthValidator(n) |
Text length <= n |
RegexValidator(pattern) |
Matches pattern |
EmailValidator() |
Valid email format |
URLValidator() |
Valid URL format |
validate_email |
Email check (function) |
validate_slug |
URL-safe string |
Complete Example: Registration Form
Let’s put it ALL together!
from django import forms
from django.core.validators import (
MinLengthValidator,
RegexValidator
)
from django.core.exceptions import ValidationError
# Custom validator
def validate_not_common_password(value):
common = ['password', '123456', 'qwerty']
if value.lower() in common:
raise ValidationError(
"Please choose a stronger password!"
)
class RegistrationForm(forms.Form):
username = forms.CharField(
max_length=30,
validators=[
MinLengthValidator(3),
RegexValidator(
r'^[a-zA-Z0-9_]+#x27;,
"Letters, numbers, underscore only"
)
]
)
email = forms.EmailField()
password = forms.CharField(
widget=forms.PasswordInput,
validators=[
MinLengthValidator(8),
validate_not_common_password
]
)
confirm_password = forms.CharField(
widget=forms.PasswordInput
)
age = forms.IntegerField()
# Field-level validation
def clean_username(self):
username = self.cleaned_data['username']
if User.objects.filter(
username__iexact=username
).exists():
raise ValidationError(
"Username already taken!"
)
return username.lower()
def clean_age(self):
age = self.cleaned_data['age']
if age < 13:
raise ValidationError(
"Must be 13 or older."
)
return age
# Form-level validation
def clean(self):
cleaned_data = super().clean()
pw = cleaned_data.get('password')
confirm = cleaned_data.get('confirm_password')
if pw and confirm and pw != confirm:
raise ValidationError(
"Passwords don't match!"
)
return cleaned_data
Summary: Validation Flow
graph TD A["Form Submitted"] --> B["is_valid Called"] B --> C["Field Validators Run"] C --> D["clean_fieldname Methods"] D --> E["clean Method"] E --> F{All Pass?} F -->|Yes| G["cleaned_data Ready!"] F -->|No| H["form.errors Populated"] G --> I["Safe to Save"] H --> J["Show Errors to User"]
Remember:
- 🔒
is_valid()starts everything - 📦
cleaned_data= your clean, safe data - ⚠️
form.errors= problems found - 🎯
clean_<field>()= check one field - 🔗
clean()= check multiple fields together - ⚡ Custom validators = reusable checks
- 🧰 Built-in validators = ready to use!
Quick Reference
# Check if form is valid
if form.is_valid():
data = form.cleaned_data
# Get errors
form.errors # All errors
form.errors['field'] # Field errors
# Field validation
def clean_username(self):
value = self.cleaned_data['username']
# ... validate ...
return value
# Form validation
def clean(self):
data = super().clean()
# ... cross-field checks ...
return data
# Raise error
raise forms.ValidationError("Message")
# Built-in validators
from django.core.validators import *
You’ve got this! 🚀
