Django

Forms — the basics

How Django turns "here's some HTML input fields" into validated Python data — and the one line (CSRF) that's easy to forget and immediately breaks every form.

Beginner

3 min read

The problem a Form class solves

Handling a submitted form by hand means reading raw strings out of request.POST, checking each one is present and the right shape, converting types, and re-showing the form with error messages if something's wrong — real, repetitive work for every single form in an application. Django's forms.Form handles all of it from one declarative class definition.

Defining a form

# forms.py
from django import forms
 
class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)

Each field declares both its expected type and its validation rules — EmailField doesn't just accept text, it actually validates that the text looks like an email address, and CharField(max_length=100) rejects anything longer. This mirrors how model fields work (covered in the models and migrations lesson) — declare the shape once, get validation for free, rather than writing if checks by hand for every field.

Using a form in a view

# views.py
from django.shortcuts import render, redirect
from .forms import ContactForm
 
def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            name = form.cleaned_data["name"]
            email = form.cleaned_data["email"]
            # do something with the validated data (save it, email it, etc.)
            return redirect("thank-you")
    else:
        form = ContactForm()
    return render(request, "contact.html", {"form": form})

This is the standard shape almost every Django form-handling view follows: on GET, show an empty form; on POST, build the form from the submitted data and check .is_valid(). If valid, form.cleaned_data holds the validated, type-converted values (an actual Python string for name, a validated email string for email — not raw, unchecked request data). If invalid, re-rendering the same template with the same form object automatically shows the submitted values back to the user along with specific error messages, without writing that logic by hand.

Rendering a form in a template

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Send</button>
</form>

{{ form.as_p }} renders every field as HTML automatically, each wrapped in a <p> tag, including labels and any validation errors from a previous failed submission — no need to hand-write <input> tags for every field (though that's also possible, field by field, when more control over the markup is needed). {% csrf_token %} inserts a hidden, unique token tied to the user's session — this line is not optional. Django rejects any POST request that's missing a valid CSRF token, specifically to prevent Cross-Site Request Forgery (a malicious site tricking a logged-in user's browser into submitting a form to your site without their knowledge). Forgetting {% csrf_token %} is one of the most common early Django form bugs — the form renders fine, but every submission fails with a 403 Forbidden error.

ModelForm: a form generated directly from a model

from django import forms
from .models import Article
 
class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body"]

When a form's job is "create or edit a specific model instance," writing every field out by hand duplicates what the model already declares. ModelForm generates the form's fields directly from the model's fields — and calling form.save() on a valid ModelForm creates or updates the actual database row directly, without manually copying each value out of cleaned_data into a new model instance. This is the far more common pattern in real Django code than a plain forms.Form — plain Form is for data that isn't tied to a single model at all, like the contact form above.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What's inside form.cleaned_data after form.is_valid() returns True?

2. What actually breaks if {% csrf_token %} is missing from a Django form template?

3. When should you use ModelForm instead of a plain Form?

4. What does EmailField actually validate, beyond just being a text input?