LESSON 77 · ADVANCED4 min read

Templates and Forms

Build consistent pages with template inheritance, validate user input with forms, and handle POST requests without losing errors.

Python 3.12+Free to readModule 17 of 19
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Use template inheritance to keep layouts consistent.
  • Create forms that validate and render field errors.
  • Handle POST with CSRF protection and a redirect after success.
  • Keep templates free of business logic.

01Template inheritance keeps layout consistent

A base template defines the shared shell and named blocks. Child templates extend it and override only the blocks they need. This avoids duplicating navigation, metadata, and footer markup on every page. A change to the shell happens in one file.

Django templates escape variables by default, which prevents accidental HTML injection. Use the safe filter only for content you have deliberately sanitized. Keep loops and conditionals simple; if a template needs complex calculations, move that work into the view or a model method.

templates/blog/base.htmlHTML
<!doctype html><html lang="en"><head>  <meta charset="utf-8">  <title>{% block title %}My Blog{% endblock %}</title></head><body>  <header><a href="{% url 'post_list' %}">Home</a></header>  <main>{% block content %}{% endblock %}</main></body></html>

02Forms validate at the boundary

A Django Form declares fields and validation rules. On a POST request, bind the form to request.POST and call is_valid(). If it passes, cleaned_data contains converted, validated values. If it fails, the same form renders with field-level errors so the user can correct the input.

A ModelForm derives fields from a model, reducing duplication. It still needs an explicit fields list; using __all__ can expose fields you did not intend to accept. Save only after validation, and perform authorization checks separately from validation.

forms.pyPython
from django import formsfrom .models import Postclass PostForm(forms.ModelForm):    class Meta:        model = Post        fields = ["title", "slug", "body", "published"]    def clean_title(self):        title = self.cleaned_data["title"].strip()        if len(title) < 3:            raise forms.ValidationError("title must be at least 3 characters")        return title

03Handle POST safely

A state-changing form must use method='post' and include {% csrf_token %}. Django validates the token and rejects requests without it in the default middleware configuration. Do not disable CSRF to make a form work; fix the template or the request instead.

After a successful POST, redirect to a GET view. This is the post/redirect/get pattern: refreshing the confirmation page does not resubmit the form. On failure, re-render the form with errors and the submitted values so the user does not lose their work.

post_create.pyPython
from django.shortcuts import redirect, renderfrom .forms import PostFormdef post_create(request):    if request.method == "POST":        form = PostForm(request.POST)        if form.is_valid():            post = form.save()            return redirect("post_detail", slug=post.slug)    else:        form = PostForm()    return render(request, "blog/post_form.html", {"form": form})

04Keep templates dumb

Templates are for presentation. Put authorization, calculations, and multi-step rules in views, forms, or model methods. A template tag is appropriate for a genuinely presentational concern that recurs, not for hiding a query or a permission check.

Test the form directly with valid and invalid data, then test the view with Django's test client. The form tests document the validation contract; the view tests document status codes, redirects, and which template rendered. Together they catch the boundary mistakes that template inspection cannot. If a template needs an expensive value, compute it once in the view and pass it in explicitly.

test_post_form.pyPython
from django.test import TestCasefrom .forms import PostFormclass PostFormTests(TestCase):    def test_rejects_short_title(self):        form = PostForm(data={"title": "Hi", "slug": "hi", "body": "text"})        self.assertFalse(form.is_valid())        self.assertIn("title", form.errors)    def test_accepts_valid_post(self):        form = PostForm(data={"title": "Hello", "slug": "hello", "body": "text"})        self.assertTrue(form.is_valid())
LET IT CLICK

The ideas worth keeping.

  • Template inheritance keeps shared markup in one place.
  • Forms validate at the boundary and render actionable errors.
  • POST, validate, redirect on success; re-render with errors on failure.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Create a contact form

Define a ContactForm with name, email, and message fields. Make email use EmailField and message use a textarea. Render it with CSRF protection.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
from django import formsclass ContactForm(forms.Form):    name = forms.CharField(max_length=100)    email = forms.EmailField()    message = forms.CharField(widget=forms.Textarea)

EmailField validates the address shape, and the textarea widget makes the message field usable. In the template, render {% csrf_token %} inside a POST form and call {{ form.as_p }} so field errors are visible.

02

Validate a post title

Add a clean_title method that trims whitespace and rejects titles shorter than three characters. Show the error path in a short explanation.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
from django import formsfrom .models import Postclass PostForm(forms.ModelForm):    class Meta:        model = Post        fields = ["title", "slug", "body"]    def clean_title(self):        title = self.cleaned_data["title"].strip()        if len(title) < 3:            raise forms.ValidationError("title must be at least 3 characters")        return title

clean_<field> runs after the field's built-in validation and before the form is considered valid. Returning the cleaned value ensures the trimmed version is what gets saved.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What does template inheritance avoid?
02What does is_valid() do?
03Why redirect after a successful POST?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Create a contact form

Define a ContactForm with name, email, and message fields. Make email use EmailField and message use a textarea. Render it with CSRF protection.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
main.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.