Django forms handle data collection, validation, and error display. They're one of Django's most powerful features.
ModelForm
The easiest way to create forms from your models:
Python
# posts/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .models import Post, Comment
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'slug', 'body', 'tags', 'published']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-input',
'placeholder': 'Enter post title...'
}),
'body': forms.Textarea(attrs={
'class': 'form-textarea',
'rows': 10,
'placeholder': 'Write your post content in Markdown...'
}),
'slug': forms.TextInput(attrs={
'class': 'form-input',
'placeholder': 'url-friendly-slug'
}),
}
def clean_title(self):
title = self.cleaned_data['title']
if len(title) < 5:
raise ValidationError('Title must be at least 5 characters long.')
return title
def clean_slug(self):
slug = self.cleaned_data['slug']
# Check uniqueness only on create (not edit)
if not self.instance.pk and Post.objects.filter(slug=slug).exists():
raise ValidationError('A post with this slug already exists.')
return slug
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['body']
widgets = {
'body': forms.Textarea(attrs={
'class': 'form-textarea',
'rows': 4,
'placeholder': 'Write your comment...'
}),
}Regular Django Form
Python
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
subject = forms.ChoiceField(choices=[
('general', 'General Inquiry'),
('bug', 'Bug Report'),
('feature', 'Feature Request'),
])
message = forms.CharField(widget=forms.Textarea(attrs={'rows': 5}))
def clean(self):
cleaned_data = super().clean()
name = cleaned_data.get('name')
message = cleaned_data.get('message')
if name and message and name.lower() in message.lower():
raise ValidationError('Message cannot contain your name.')
return cleaned_dataForm Validation Flow
Code
1. User submits form (POST request)
2. form = PostForm(request.POST) ← Bind data to form
3. form.is_valid() ← Runs ALL validation:
a. field.clean_<fieldname>() ← Per-field validators
b. clean_<fieldname>() ← Custom field validation
c. clean() ← Cross-field validation
4. If valid: form.cleaned_data ← Cleaned, typed data
5. If invalid: form.errors ← Dict of error messagesDisplaying Forms in Templates
HTML
<!-- templates/posts/post_form.html -->
{% extends "base.html" %}
{% block content %}
<h1>{{ action }} Post</h1>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
{% endfor %}
{% if form.non_field_errors %}
<div class="errors">
{% for error in form.non_field_errors %}
<p class="error">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<button type="submit">{{ action }} Post</button>
</form>
{% endblock %}Formset — Multiple Forms at Once
Python
# Inline formsets for comments on a post
from django.forms import inlineformset_factory
CommentFormSet = inlineformset_factory(
Post, Comment,
fields=['body', 'active'],
extra=1, # Number of empty forms to show
can_delete=True, # Show checkboxes for deletion
)Python
# In your view
def post_edit(request, slug):
post = get_object_or_404(Post, slug=slug)
CommentFormSet = inlineformset_factory(Post, Comment, fields=['body', 'active'])
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
formset = CommentFormSet(request.POST, instance=post)
if form.is_valid() and formset.is_valid():
form.save()
formset.save()
return redirect('posts:detail', slug=post.slug)
else:
form = PostForm(instance=post)
formset = CommentFormSet(instance=post)
return render(request, 'posts/post_edit.html', {
'form': form,
'formset': formset,
})File Uploads
Python
class UploadForm(forms.Form):
title = forms.CharField(max_length=200)
file = forms.FileField()
# In view
def upload_file(request):
if request.method == 'POST':
form = UploadForm(request.POST, request.FILES) # Don't forget request.FILES!
if form.is_valid():
uploaded = request.FILES['file']
# Save to model
document = Document(
title=form.cleaned_data['title'],
file=uploaded,
size=uploaded.size,
)
document.save()
return redirect('documents:list')
else:
form = UploadForm()
return render(request, 'upload.html', {'form': form})Common Validation Patterns
Python
# validators.py
from django.core.validators import RegexValidator
# Username validation
username_validator = RegexValidator(
regex=r'^[a-zA-Z0-9_]+$',
message='Username can only contain letters, numbers, and underscores.'
)
# File size limit
def validate_file_size(value):
limit = 5 * 1024 * 1024 # 5MB
if value.size > limit:
raise ValidationError('File too large. Maximum size is 5MB.')💡 Tip: Always use
novalidateon your<form>tag and validate on the server side. Client-side validation is for UX, server-side validation is for security.
Next: Models and Database