Django's form validation system provides multiple layers of data validation, from field-level checks to complex form-wide business rules. Understanding validation patterns enables you to build robust forms that ensure data integrity and provide meaningful user feedback.
# forms.py - Validation flow demonstration
from django import forms
from django.core.exceptions import ValidationError
class ComprehensiveValidationForm(forms.Form):
"""Form demonstrating the complete validation hierarchy"""
username = forms.CharField(max_length=30)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
age = forms.IntegerField()
def clean_username(self):
"""Step 1: Field-level validation"""
username = self.cleaned_data['username']
print(f"1. Field validation for username: {username}")
# Length validation
if len(username) < 3:
raise ValidationError('Username must be at least 3 characters long.')
# Character validation
if not username.isalnum():
raise ValidationError('Username must contain only letters and numbers.')
# Availability check
from django.contrib.auth.models import User
if User.objects.filter(username=username).exists():
raise ValidationError('This username is already taken.')
return username
def clean_email(self):
"""Step 1: Field-level validation for email"""
email = self.cleaned_data['email']
print(f"1. Field validation for email: {email}")
# Domain validation
allowed_domains = ['gmail.com', 'yahoo.com', 'company.com']
domain = email.split('@')[1]
if domain not in allowed_domains:
raise ValidationError(f'Email domain {domain} is not allowed.')
return email
def clean_password(self):
"""Step 1: Field-level validation for password"""
password = self.cleaned_data['password']
print(f"1. Field validation for password")
# Length check
if len(password) < 8:
raise ValidationError('Password must be at least 8 characters long.')
# Complexity check
if password.isdigit():
raise ValidationError('Password cannot be entirely numeric.')
# Common password check
common_passwords = ['password', '12345678', 'qwerty', 'abc123']
if password.lower() in common_passwords:
raise ValidationError('Password is too common.')
return password
def clean_age(self):
"""Step 1: Field-level validation for age"""
age = self.cleaned_data['age']
print(f"1. Field validation for age: {age}")
if age < 13:
raise ValidationError('You must be at least 13 years old.')
if age > 120:
raise ValidationError('Please enter a realistic age.')
return age
def clean(self):
"""Step 2: Form-level validation"""
cleaned_data = super().clean()
print("2. Form-level validation")
password = cleaned_data.get('password')
confirm_password = cleaned_data.get('confirm_password')
username = cleaned_data.get('username')
# Password confirmation
if password and confirm_password:
if password != confirm_password:
raise ValidationError('Passwords do not match.')
# Username-password similarity
if username and password:
if username.lower() in password.lower():
raise ValidationError('Password cannot contain the username.')
return cleaned_data
def full_clean(self):
"""Step 3: Complete form validation (rarely overridden)"""
print("3. Full form validation")
super().full_clean()
# Additional business logic validation
self._validate_business_rules()
def _validate_business_rules(self):
"""Custom business rule validation"""
print("4. Business rules validation")
if hasattr(self, 'cleaned_data'):
username = self.cleaned_data.get('username')
email = self.cleaned_data.get('email')
# Business rule: username cannot be part of email
if username and email and username in email:
self.add_error(None, 'Username cannot be part of email address.')
# forms.py - Using built-in validators
from django import forms
from django.core.validators import (
RegexValidator, EmailValidator, URLValidator,
MinLengthValidator, MaxLengthValidator,
MinValueValidator, MaxValueValidator,
DecimalValidator, validate_email
)
class ValidatorExamplesForm(forms.Form):
"""Examples of built-in validators"""
# Regex validation
phone = forms.CharField(
max_length=15,
validators=[
RegexValidator(
regex=r'^\+?1?\d{9,15}$',
message='Phone number must be in format: "+999999999". Up to 15 digits allowed.'
)
]
)
# Multiple validators on one field
username = forms.CharField(
validators=[
MinLengthValidator(3, message='Username must be at least 3 characters.'),
MaxLengthValidator(20, message='Username cannot exceed 20 characters.'),
RegexValidator(
regex=r'^[a-zA-Z0-9_]+$',
message='Username can only contain letters, numbers, and underscores.'
)
]
)
# Numeric validators
age = forms.IntegerField(
validators=[
MinValueValidator(13, message='You must be at least 13 years old.'),
MaxValueValidator(120, message='Please enter a realistic age.')
]
)
# Decimal validation
price = forms.DecimalField(
max_digits=10,
decimal_places=2,
validators=[
MinValueValidator(0.01, message='Price must be at least $0.01.'),
MaxValueValidator(999999.99, message='Price cannot exceed $999,999.99.')
]
)
# URL validation with custom message
website = forms.URLField(
required=False,
validators=[
URLValidator(
message='Please enter a valid URL starting with http:// or https://'
)
]
)
# Email validation
email = forms.CharField( # Using CharField instead of EmailField for custom validation
validators=[
EmailValidator(message='Please enter a valid email address.')
]
)
# validators.py - Custom validator functions
from django.core.exceptions import ValidationError
import re
def validate_strong_password(password):
"""Validate password strength"""
if len(password) < 8:
raise ValidationError('Password must be at least 8 characters long.')
if not re.search(r'[A-Z]', password):
raise ValidationError('Password must contain at least one uppercase letter.')
if not re.search(r'[a-z]', password):
raise ValidationError('Password must contain at least one lowercase letter.')
if not re.search(r'\d', password):
raise ValidationError('Password must contain at least one digit.')
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
raise ValidationError('Password must contain at least one special character.')
def validate_file_size(file):
"""Validate uploaded file size"""
max_size = 5 * 1024 * 1024 # 5MB
if file.size > max_size:
raise ValidationError(f'File size cannot exceed 5MB. Current size: {file.size / (1024*1024):.1f}MB')
def validate_image_dimensions(image):
"""Validate image dimensions"""
max_width = 1920
max_height = 1080
if image.width > max_width or image.height > max_height:
raise ValidationError(
f'Image dimensions cannot exceed {max_width}x{max_height}px. '
f'Current dimensions: {image.width}x{image.height}px'
)
def validate_profanity(text):
"""Basic profanity filter"""
profanity_words = ['badword1', 'badword2', 'badword3'] # Replace with actual list
text_lower = text.lower()
for word in profanity_words:
if word in text_lower:
raise ValidationError('Content contains inappropriate language.')
def validate_business_hours(time_value):
"""Validate time is within business hours"""
from datetime import time
business_start = time(9, 0) # 9:00 AM
business_end = time(17, 0) # 5:00 PM
if not (business_start <= time_value <= business_end):
raise ValidationError('Time must be within business hours (9:00 AM - 5:00 PM).')
# forms.py - Using custom validators
class AdvancedValidationForm(forms.Form):
"""Form using custom validators"""
password = forms.CharField(
widget=forms.PasswordInput,
validators=[validate_strong_password]
)
profile_image = forms.ImageField(
validators=[validate_file_size, validate_image_dimensions]
)
bio = forms.CharField(
widget=forms.Textarea,
validators=[validate_profanity]
)
appointment_time = forms.TimeField(
validators=[validate_business_hours]
)
# forms.py - Complex form-level validation
from django import forms
from django.core.exceptions import ValidationError
from datetime import date, datetime, timedelta
class EventBookingForm(forms.Form):
"""Event booking with complex validation rules"""
event_name = forms.CharField(max_length=200)
start_date = forms.DateField()
end_date = forms.DateField()
start_time = forms.TimeField()
end_time = forms.TimeField()
attendees = forms.IntegerField(min_value=1)
budget = forms.DecimalField(max_digits=10, decimal_places=2)
# Room selection
ROOM_CHOICES = [
('small', 'Small Room (10 people) - $100/day'),
('medium', 'Medium Room (25 people) - $200/day'),
('large', 'Large Room (50 people) - $300/day'),
('auditorium', 'Auditorium (100+ people) - $500/day'),
]
room_type = forms.ChoiceField(choices=ROOM_CHOICES)
# Additional services
catering = forms.BooleanField(required=False, label='Catering Required')
av_equipment = forms.BooleanField(required=False, label='A/V Equipment')
parking = forms.BooleanField(required=False, label='Reserved Parking')
def clean(self):
"""Comprehensive form-level validation"""
cleaned_data = super().clean()
# Date validation
self._validate_dates(cleaned_data)
# Time validation
self._validate_times(cleaned_data)
# Capacity validation
self._validate_capacity(cleaned_data)
# Budget validation
self._validate_budget(cleaned_data)
# Availability validation
self._validate_availability(cleaned_data)
return cleaned_data
def _validate_dates(self, data):
"""Validate date range"""
start_date = data.get('start_date')
end_date = data.get('end_date')
if start_date and end_date:
# End date must be after start date
if end_date < start_date:
raise ValidationError('End date must be after start date.')
# Cannot book in the past
if start_date < date.today():
raise ValidationError('Cannot book events in the past.')
# Maximum booking duration
if (end_date - start_date).days > 30:
raise ValidationError('Events cannot exceed 30 days.')
# Minimum advance booking
if start_date < date.today() + timedelta(days=2):
raise ValidationError('Events must be booked at least 2 days in advance.')
def _validate_times(self, data):
"""Validate time range"""
start_time = data.get('start_time')
end_time = data.get('end_time')
start_date = data.get('start_date')
end_date = data.get('end_date')
if start_time and end_time and start_date and end_date:
# For same-day events, end time must be after start time
if start_date == end_date and end_time <= start_time:
raise ValidationError('End time must be after start time for same-day events.')
# Business hours validation
from datetime import time
business_start = time(8, 0)
business_end = time(22, 0)
if not (business_start <= start_time <= business_end):
raise ValidationError('Start time must be within business hours (8:00 AM - 10:00 PM).')
if not (business_start <= end_time <= business_end):
raise ValidationError('End time must be within business hours (8:00 AM - 10:00 PM).')
def _validate_capacity(self, data):
"""Validate room capacity vs attendees"""
room_type = data.get('room_type')
attendees = data.get('attendees')
if room_type and attendees:
capacity_limits = {
'small': 10,
'medium': 25,
'large': 50,
'auditorium': 200
}
max_capacity = capacity_limits.get(room_type, 0)
if attendees > max_capacity:
raise ValidationError(
f'Selected room can accommodate maximum {max_capacity} people. '
f'You have {attendees} attendees.'
)
def _validate_budget(self, data):
"""Validate budget against estimated costs"""
room_type = data.get('room_type')
budget = data.get('budget')
start_date = data.get('start_date')
end_date = data.get('end_date')
catering = data.get('catering', False)
av_equipment = data.get('av_equipment', False)
parking = data.get('parking', False)
attendees = data.get('attendees', 0)
if all([room_type, budget, start_date, end_date]):
# Calculate estimated cost
room_costs = {
'small': 100,
'medium': 200,
'large': 300,
'auditorium': 500
}
days = (end_date - start_date).days + 1
room_cost = room_costs.get(room_type, 0) * days
additional_costs = 0
if catering:
additional_costs += attendees * 25 # $25 per person
if av_equipment:
additional_costs += 150 * days
if parking:
additional_costs += 50 * days
total_estimated_cost = room_cost + additional_costs
if budget < total_estimated_cost:
raise ValidationError(
f'Budget (${budget}) is insufficient. '
f'Estimated cost: ${total_estimated_cost} '
f'(Room: ${room_cost}, Additional: ${additional_costs})'
)
def _validate_availability(self, data):
"""Check room availability (simplified)"""
room_type = data.get('room_type')
start_date = data.get('start_date')
end_date = data.get('end_date')
if all([room_type, start_date, end_date]):
# In a real application, check against booking database
# This is a simplified example
# Simulate checking availability
from .models import Booking
conflicting_bookings = Booking.objects.filter(
room_type=room_type,
start_date__lte=end_date,
end_date__gte=start_date
)
if conflicting_bookings.exists():
raise ValidationError(
f'Selected room is not available for the chosen dates. '
f'Please select different dates or room type.'
)
# forms.py - Dynamic validation based on form state
from django import forms
class DynamicValidationForm(forms.Form):
"""Form with validation rules that change based on user input"""
USER_TYPES = [
('individual', 'Individual'),
('business', 'Business'),
('nonprofit', 'Non-Profit Organization'),
]
user_type = forms.ChoiceField(choices=USER_TYPES)
# Common fields
email = forms.EmailField()
phone = forms.CharField(max_length=15)
# Individual fields
first_name = forms.CharField(max_length=30, required=False)
last_name = forms.CharField(max_length=30, required=False)
date_of_birth = forms.DateField(required=False)
# Business fields
company_name = forms.CharField(max_length=100, required=False)
tax_id = forms.CharField(max_length=20, required=False)
annual_revenue = forms.DecimalField(max_digits=12, decimal_places=2, required=False)
# Non-profit fields
organization_name = forms.CharField(max_length=100, required=False)
nonprofit_id = forms.CharField(max_length=20, required=False)
mission_statement = forms.CharField(widget=forms.Textarea, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set initial field requirements based on data
if self.data:
user_type = self.data.get('user_type')
self._update_field_requirements(user_type)
def _update_field_requirements(self, user_type):
"""Update field requirements based on user type"""
if user_type == 'individual':
self.fields['first_name'].required = True
self.fields['last_name'].required = True
self.fields['date_of_birth'].required = True
elif user_type == 'business':
self.fields['company_name'].required = True
self.fields['tax_id'].required = True
self.fields['annual_revenue'].required = True
elif user_type == 'nonprofit':
self.fields['organization_name'].required = True
self.fields['nonprofit_id'].required = True
self.fields['mission_statement'].required = True
def clean(self):
cleaned_data = super().clean()
user_type = cleaned_data.get('user_type')
# Apply type-specific validation
if user_type == 'individual':
self._validate_individual(cleaned_data)
elif user_type == 'business':
self._validate_business(cleaned_data)
elif user_type == 'nonprofit':
self._validate_nonprofit(cleaned_data)
return cleaned_data
def _validate_individual(self, data):
"""Validation specific to individual users"""
date_of_birth = data.get('date_of_birth')
if date_of_birth:
from datetime import date
today = date.today()
age = today.year - date_of_birth.year - ((today.month, today.day) < (date_of_birth.month, date_of_birth.day))
if age < 18:
raise ValidationError('Individual users must be at least 18 years old.')
def _validate_business(self, data):
"""Validation specific to business users"""
tax_id = data.get('tax_id')
annual_revenue = data.get('annual_revenue')
if tax_id:
# Validate tax ID format (simplified)
import re
if not re.match(r'^\d{2}-\d{7}$', tax_id):
raise ValidationError('Tax ID must be in format: XX-XXXXXXX')
if annual_revenue and annual_revenue < 0:
raise ValidationError('Annual revenue cannot be negative.')
def _validate_nonprofit(self, data):
"""Validation specific to nonprofit organizations"""
nonprofit_id = data.get('nonprofit_id')
mission_statement = data.get('mission_statement')
if nonprofit_id:
# Validate nonprofit ID format
if not nonprofit_id.startswith('NP'):
raise ValidationError('Non-profit ID must start with "NP".')
if mission_statement and len(mission_statement) < 50:
raise ValidationError('Mission statement must be at least 50 characters long.')
# forms.py - Advanced error handling
from django import forms
from django.core.exceptions import ValidationError
class ErrorHandlingForm(forms.Form):
"""Form demonstrating advanced error handling techniques"""
username = forms.CharField(max_length=30)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
# Custom error messages
error_messages = {
'username': {
'required': 'Please provide a username.',
'max_length': 'Username cannot exceed 30 characters.',
},
'email': {
'required': 'Email address is required.',
'invalid': 'Please enter a valid email address.',
},
'password': {
'required': 'Password is required.',
}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Apply custom error messages
for field_name, messages in self.error_messages.items():
if field_name in self.fields:
self.fields[field_name].error_messages.update(messages)
def clean_username(self):
username = self.cleaned_data['username']
# Multiple validation checks with specific error messages
errors = []
if len(username) < 3:
errors.append('Username must be at least 3 characters long.')
if not username.isalnum():
errors.append('Username can only contain letters and numbers.')
if username.lower() in ['admin', 'root', 'user']:
errors.append('This username is reserved and cannot be used.')
# Check availability
from django.contrib.auth.models import User
if User.objects.filter(username=username).exists():
errors.append('This username is already taken.')
if errors:
raise ValidationError(errors)
return username
def add_error(self, field, error):
"""Override to customize error handling"""
# Log validation errors
import logging
logger = logging.getLogger('form_validation')
logger.warning(f'Validation error in {self.__class__.__name__}.{field}: {error}')
# Call parent method
super().add_error(field, error)
def clean(self):
cleaned_data = super().clean()
# Collect all validation errors
validation_errors = []
username = cleaned_data.get('username')
password = cleaned_data.get('password')
# Cross-field validation with detailed errors
if username and password:
if username.lower() in password.lower():
validation_errors.append(
ValidationError(
'Password cannot contain the username.',
code='password_contains_username'
)
)
if len(password) < 8:
validation_errors.append(
ValidationError(
'Password must be at least 8 characters long.',
code='password_too_short'
)
)
if validation_errors:
raise ValidationError(validation_errors)
return cleaned_data
# Custom validation error class
class DetailedValidationError(ValidationError):
"""Enhanced validation error with additional context"""
def __init__(self, message, code=None, params=None, field=None, suggestion=None):
super().__init__(message, code, params)
self.field = field
self.suggestion = suggestion
# Usage in forms
class EnhancedForm(forms.Form):
email = forms.EmailField()
def clean_email(self):
email = self.cleaned_data['email']
if not email.endswith('@company.com'):
raise DetailedValidationError(
'Only company email addresses are allowed.',
code='invalid_domain',
field='email',
suggestion='Please use your @company.com email address.'
)
return email
# forms.py - Async validation patterns
from django import forms
import asyncio
import aiohttp
class AsyncValidationForm(forms.Form):
"""Form with asynchronous validation"""
username = forms.CharField(max_length=30)
email = forms.EmailField()
domain = forms.CharField(max_length=100)
async def clean_username_async(self):
"""Async username validation"""
username = self.cleaned_data['username']
# Simulate API call to check username availability
async with aiohttp.ClientSession() as session:
async with session.get(f'https://api.example.com/check-username/{username}') as response:
if response.status == 200:
data = await response.json()
if not data.get('available', True):
raise ValidationError('Username is not available.')
return username
async def clean_email_async(self):
"""Async email validation"""
email = self.cleaned_data['email']
# Validate email with external service
async with aiohttp.ClientSession() as session:
async with session.post('https://api.emailvalidation.com/validate',
json={'email': email}) as response:
if response.status == 200:
data = await response.json()
if not data.get('valid', False):
raise ValidationError('Email address is not valid.')
return email
async def clean_domain_async(self):
"""Async domain validation"""
domain = self.cleaned_data['domain']
# Check if domain exists
try:
import socket
socket.gethostbyname(domain)
except socket.gaierror:
raise ValidationError('Domain does not exist.')
return domain
async def full_clean_async(self):
"""Async version of full_clean"""
# Run regular validation first
super().full_clean()
if not self.errors:
# Run async validations
tasks = []
if 'username' in self.cleaned_data:
tasks.append(self.clean_username_async())
if 'email' in self.cleaned_data:
tasks.append(self.clean_email_async())
if 'domain' in self.cleaned_data:
tasks.append(self.clean_domain_async())
try:
await asyncio.gather(*tasks)
except ValidationError as e:
self.add_error(None, e)
# views.py - Handling async validation
import asyncio
from django.http import JsonResponse
async def async_form_view(request):
if request.method == 'POST':
form = AsyncValidationForm(request.POST)
# Run async validation
await form.full_clean_async()
if form.is_valid():
# Process form
return JsonResponse({'success': True})
else:
return JsonResponse({'errors': form.errors}, status=400)
return render(request, 'async_form.html', {'form': AsyncValidationForm()})
Django's validation system provides comprehensive tools for ensuring data integrity at multiple levels. By understanding field-level validation, form-level cross-validation, dynamic validation rules, and advanced error handling techniques, you can build robust forms that provide clear feedback and maintain data quality while handling complex business requirements.
Creating Forms with Forms API
Django's Forms API provides a powerful and flexible way to define, validate, and render forms. This chapter covers the complete process of creating forms using Django's declarative form system.
Built-in Fields and Widgets
Django provides a comprehensive set of form fields and widgets that handle different data types and user interface elements. Understanding these built-in components enables you to create sophisticated forms without custom implementations.