~/TechPurAI
~/tutorials/django-from-scratch/django-forms
intermediate·part 12 of 22·2 min read

Django forms: rendering, validation, and cleaned data

Updated Aug 15, 2026Python · Django

Comments have only ever been added through the admin so far. This part builds a real Form for it — deliberately a plain forms.Form rather than a ModelForm (that's part 13), so the validation mechanics are visible before the shortcut that hides most of them.

Defining the form

python
# blog/forms.py
from django import forms

class CommentForm(forms.Form):
    name = forms.CharField(max_length=80)
    body = forms.CharField(widget=forms.Textarea)

Each field on a Form class does two jobs at once: it defines what HTML input renders (CharField → a text input, unless a different widget is set, like Textarea here), and it defines how the submitted value gets validated and cleaned.

Rendering it in a template

python
# blog/views.py
from .forms import CommentForm

class PostDetailView(DetailView):
    model = Post
    template_name = "blog/post_detail.html"
    context_object_name = "post"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["form"] = CommentForm()
        return context
django
{# inside post_detail.html's {% block content %} #}
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Post comment</button>
</form>

{{ form.as_p }} renders every field wrapped in a <p> tag, with labels generated from each field's name — the fastest way to get a form on the page, though real projects often render fields individually for more layout control. {% csrf_token %} is not optional; part 14 covers exactly why.

Handling the submission

python
def post_detail(request, slug):
    post = get_object_or_404(Post, slug=slug)
    if request.method == "POST":
        form = CommentForm(request.POST)
        if form.is_valid():
            Comment.objects.create(
                post=post,
                name=form.cleaned_data["name"],
                body=form.cleaned_data["body"],
            )
            return redirect("post_detail", slug=post.slug)
    else:
        form = CommentForm()
    return render(request, "blog/post_detail.html", {"post": post, "form": form})

This switches back to a function-based view — handling two HTTP methods differently is exactly the branching case from part 9's callout where a function view stays more readable than a class-based one. form.is_valid() runs every field's validation and returns False if anything fails; only on success is form.cleaned_data safe to read — it holds the validated, correctly-typed values (not the raw, unvalidated request.POST strings). Redirecting after a successful POST, rather than rendering a response directly, is deliberate: it stops a page refresh from resubmitting the same form.

Common mistake

Reading request.POST["name"] directly instead of form.cleaned_data["name"] after validation. It skips everything the form was just asked to check — length limits, required fields, type coercion — making the validation step you just wrote pointless.

Next: ModelForm — the shortcut that generates a form like this one directly from a model, and CreateView/UpdateView to go with it.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous11. Model relationships: ForeignKey and related lookupsnext →13. ModelForms: CreateView and UpdateView