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.
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?