Forms & Validation

Django Fundamentals — Forms & Validation
Django Fundamentals
Course 1 · Chapter 7 · Forms & Validation

📝 Forms & Validation

FastAPI's Pydantic models validate a request body but leave you to hand-write the HTML form yourself. Django's Form and ModelForm classes do both jobs at once — rendering the fields and validating what comes back — and, without any extra package, protect every form against CSRF the way an entire dedicated course covered doing by hand.

A Basic Form

A Form class describes fields the same way a model does — but these fields describe input validation, not database columns:

# catalog/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)
# catalog/views.py def contact(request): if request.method == "POST": form = ContactForm(request.POST) if form.is_valid(): data = form.cleaned_data # validated, type-converted dict # ... send an email, save a record, etc. ... return redirect("contact-success") else: form = ContactForm() return render(request, "catalog/contact.html", {"form": form})

is_valid()

Runs every field's validation — EmailField rejects a malformed address, CharField(max_length=100) rejects anything longer — and returns True/False.

form.cleaned_data

Only populated after a successful is_valid() — already type-converted (an IntegerField gives back a real int, not a string).

🧬 ModelForm: Generated From a Model

Writing a Form by hand that mirrors a model's fields is duplicated work — ModelForm generates the form directly from the model itself:

# catalog/forms.py from django import forms from .models import Book class BookForm(forms.ModelForm): class Meta: model = Book fields = ["title", "description", "price", "published_date"] # catalog/views.py def create_book(request): if request.method == "POST": form = BookForm(request.POST) if form.is_valid(): form.save() # creates AND saves a Book instance directly return redirect("book-list") else: form = BookForm() return render(request, "catalog/book_form.html", {"form": form})

form.save() both validates and persists the model instance — one line replaces manually reading each field off request.POST and constructing a Book(...) by hand.

🛡️ Built-in CSRF Protection

The entire CSRF course was dedicated to the synchronizer token pattern — generate a token, embed it in the form, validate it on submit. Django implements exactly that pattern, on by default, for every POST form:

<!-- catalog/templates/catalog/book_form.html --> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">Save</button> </form>

Django's Built-in CSRF vs Express's Manual Setup

Express: Manual Setup Required
const csrf = require("csurf"); app.use(csrf()); app.get("/form", (req, res) => { res.render("form", { csrfToken: req.csrfToken() }); }); // Requires installing a package, wiring middleware, // and manually passing the token into every template.
Django: On By Default
<!-- Just one template tag — CsrfViewMiddleware --> <!-- is already active in MIDDLEWARE by default. --> {% csrf_token %}

CsrfViewMiddleware is already in settings.py's MIDDLEWARE list from the moment startproject creates it — every POST form just needs the one template tag, and Django rejects any POST request missing a valid token with a 403, automatically.

Custom Validation

Beyond a field's built-in checks, a form can define its own validation logic — per-field with clean_<fieldname>(), or across multiple fields with clean():

class BookForm(forms.ModelForm): class Meta: model = Book fields = ["title", "price", "published_date"] def clean_price(self): price = self.cleaned_data["price"] if price <= 0: raise forms.ValidationError("Price must be greater than zero.") return price def clean(self): cleaned_data = super().clean() if cleaned_data.get("published_date") and cleaned_data["published_date"].year > 2026: raise forms.ValidationError("Published date can't be in the future.") return cleaned_data

Mass Assignment: Meta.fields as an Allowlist

Rails' Forms chapter warned about mass assignment vulnerabilities — Django's protection is ModelForm.Meta.fields acting as an explicit allowlist:

Dangerous vs Safe Meta.fields

Dangerous: Exposes Everything
class Meta: model = User fields = "__all__" # Includes is_staff, is_superuser — a user submitting # extra form fields could grant themselves admin access.
Safe: Explicit Allowlist
class Meta: model = User fields = ["username", "email"] # Only these two fields can ever be set through this form — # is_staff simply isn't reachable, no matter what's submitted.

💻 Coding Challenges

Challenge 1: Write a ModelForm

Write a AuthorForm(forms.ModelForm) for the Author model from Chapter 5, exposing only name and bio (not birth_year), then write the view function that renders it on GET and saves it on a valid POST.

Goal: Practice ModelForm's Meta.fields allowlist and the is_valid()/save() pattern.

→ Solution

Challenge 2: Add Custom Validation

Add a clean_bio() method to the AuthorForm from Challenge 1 that rejects a bio longer than 500 characters with a clear ValidationError message.

Goal: Practice field-level custom validation beyond what the field type checks automatically.

→ Solution

Challenge 3: Spot the Mass Assignment Risk

Given a ModelForm for a UserProfile model (with fields bio, avatar_url, and is_verified) that uses fields = "__all__", rewrite it to a safe explicit allowlist and explain in one sentence what real-world exploit the original version was vulnerable to.

Goal: Practice recognizing and fixing the Django equivalent of the mass assignment vulnerability the Rails course covered.

→ Solution

⚠️ Gotcha: Forgetting {% csrf_token %}

Omit {% csrf_token %} from a POST form's template and every single submission fails with 403 Forbidden: CSRF verification failed — not a silent bug, but a confusing one the first time it happens, since GET requests through the same view work fine. Because CsrfViewMiddleware is on by default project-wide, this isn't optional configuration to remember per-view the way Express's csurf setup is — it's a template detail that's easy to skip when copy-pasting a form from example code that used {{ form.as_p }} alone.

🎯 What's Next

The final chapter of this course covers the feature that's arguably Django's biggest single differentiator from FastAPI and Express: Admin Interface & Settings — the auto-generated admin site, and how settings.py ties every piece covered so far together.