Forms and Validation

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_data

Form 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 messages

Displaying 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 novalidate on 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