Forms
Reviewed & published by Brayan K
A Django form is a Python class that defines a set of fields, renders them as HTML inputs, and validates the data a user submits — turning messy, untrusted request data into clean, typed Python values you can safely use.
Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll define your own Form subclass, wire it into a view using the GET-versus-POST pattern, and add validation that catches bad input before it ever reaches your database.
What You'll Learn in This Lesson
- How to define a Form subclass with typed fields
- Choosing fields: CharField, EmailField, IntegerField, BooleanField
- Customising inputs with widgets
- The GET-vs-POST pattern inside a view
- Using is_valid() and reading cleaned_data safely
- Writing custom clean_<field> validation methods
1 Defining a Form
A Django form is a Python class that subclasses forms.Form. Each field you declare on the class becomes both an HTML input when the form is rendered and a validation rule when the form is processed.
The field type you pick decides how the value is rendered and what counts as valid:
- CharField — text; supports max_length and min_length.
- EmailField — text that must look like an email address.
- IntegerField — converts input to an int; supports min_value/max_value.
- BooleanField — a checkbox; required=False lets it be unchecked.
A widget controls the HTML used to render a field. For example, a CharField normally renders a single-line input, but giving it widget=forms.Textarea renders a multi-line box instead.
Here is a real Django form declaration:
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
age = forms.IntegerField(min_value=13, max_value=120)
subscribe = forms.BooleanField(required=False)
message = forms.CharField(
widget=forms.Textarea,
max_length=500,
)# Django's forms.Field machinery is large, but the core idea is simple:
# each field knows its type and rules. Here's a tiny stand-in that mimics
# how a Form describes its fields so you can see the shape of it.
class Field:
def __init__(self, kind, required=True, max_length=None):
self.kind = kind
self.required = required
self.max_length = max_length
class ContactForm:
name = Field("CharField", max_length=100)
email = Field("EmailField")
age = Field("IntegerField")
subscribe = Field("BooleanField", required=False)
# Inspect the declared fields, just like Django does internally
for field_name in ("name", "email", "age", "subscribe"):
field = getattr(ContactForm, field_name)
flag = "required" if field.required else "optional"
print(f"{field_name:10} -> {field.kind:14} ({flag})")
# ✅ Expected output:
# name -> CharField (required)
# email -> EmailField (required)
# age -> IntegerField (required)
# subscribe -> BooleanField (optional)2 Handling a Form in a View
The same view usually handles both showing an empty form and processing a submitted one. The deciding factor is the HTTP method:
- GET — the user is just visiting the page; show a blank, unbound form.
- POST — the user submitted data; build a bound form from request.POST and validate it.
Calling form.is_valid() runs validation and, when it returns True, fills form.cleaned_data — a dictionary of converted, trusted Python values keyed by field name.
This is the canonical view pattern:
from django.shortcuts import render, redirect
def contact_view(request):
if request.method == "POST":
form = ContactForm(request.POST) # bound form
if form.is_valid():
name = form.cleaned_data["name"]
email = form.cleaned_data["email"]
# ... send an email, save to the DB, etc.
return redirect("thanks")
# invalid: fall through and re-render the bound form with errors
else:
form = ContactForm() # unbound, empty form
return render(request, "contact.html", {"form": form})The matching template renders the form and the CSRF token:
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Send</button>
</form>Notice the template uses {% csrf_token %} and {{ form.as_p }}. The as_p helper wraps each field in a paragraph; alternatives are as_table and as_ul.
# Let's simulate the GET-vs-POST branching with a fake request and a tiny
# form that validates a dict of input the way Django validates request.POST.
class SimpleForm:
def __init__(self, data=None):
self.data = data # None => unbound, like an empty GET form
self.cleaned_data = {}
self.errors = {}
def is_bound(self):
return self.data is not None
def is_valid(self):
if not self.is_bound():
return False
name = (self.data.get("name") or "").strip()
if not name:
self.errors["name"] = "This field is required."
return False
self.cleaned_data["name"] = name
return True
def contact_view(request):
if request["method"] == "POST":
form = SimpleForm(request.get("POST"))
if form.is_valid():
return f"Saved! Hello, {form.cleaned_data['name']}."
return f"Invalid form, errors: {form.errors}"
else:
form = SimpleForm() # unbound
return "Rendering empty form (GET request)."
# Simulate a GET, then a good POST, then a bad POST
print(contact_view({"method": "GET"}))
print(contact_view({"method": "POST", "POST": {"name": "Ada"}}))
print(contact_view({"method": "POST", "POST": {"name": ""}}))
# ✅ Expected output:
# Rendering empty form (GET request).
# Saved! Hello, Ada.
# Invalid form, errors: {'name': 'This field is required.'}3 Validation
Validation happens in layers. First, each field checks the basics — a required field must have a value, an EmailField must look like an email, an IntegerField must parse as a number. You can add reusable validators to a field, and you can write a clean_<field> method for custom, per-field logic.
A clean_<field> method reads the value from self.cleaned_data, raises forms.ValidationError if something is wrong, and must return the value so it stays in cleaned_data:
from django import forms
from django.core.validators import MinLengthValidator
class SignupForm(forms.Form):
username = forms.CharField(
max_length=30,
validators=[MinLengthValidator(3)], # reusable validator
)
age = forms.IntegerField(min_value=0)
def clean_username(self):
value = self.cleaned_data["username"]
if value.lower() in ("admin", "root"):
raise forms.ValidationError("That username is reserved.")
return value # ALWAYS return the cleaned value# Here's a runnable model of how Django validates a field, applies a
# field validator, and then runs a custom clean_<field> method.
class ValidationError(Exception):
pass
class SignupForm:
RESERVED = ("admin", "root")
def __init__(self, data):
self.data = data
self.cleaned_data = {}
self.errors = {}
def is_valid(self):
# 1. required + type check for "username"
username = (self.data.get("username") or "").strip()
if not username:
self.errors["username"] = "This field is required."
elif len(username) < 3: # min-length validator
self.errors["username"] = "Must be at least 3 characters."
else:
try:
self.cleaned_data["username"] = self.clean_username(username)
except ValidationError as exc:
self.errors["username"] = str(exc)
return not self.errors
def clean_username(self, value):
if value.lower() in self.RESERVED:
raise ValidationError("That username is reserved.")
return value # return so it survives into cleaned_data
for sample in ({"username": "Ada"}, {"username": "ad"}, {"username": "admin"}):
form = SignupForm(sample)
if form.is_valid():
print("OK ->", form.cleaned_data)
else:
print("BAD ->", form.errors)
# ✅ Expected output:
# OK -> {'username': 'Ada'}
# BAD -> {'username': 'Must be at least 3 characters.'}
# BAD -> {'username': 'That username is reserved.'}🎯 Your Turn: Gate on Validation
Fill in the blanks so the view only reads cleaned_data after the form reports that it is valid.
# 🎯 YOUR TURN: replace the two ___ blanks.
# 1) call the method that runs validation
# 2) read the trusted value dictionary
class SimpleForm:
def __init__(self, data):
self.data = data
self.cleaned_data = {}
def is_valid(self):
name = (self.data.get("name") or "").strip()
if name:
self.cleaned_data["name"] = name
return True
return False
def handle(data):
form = SimpleForm(data)
if form.___(): # blank 1: the gatekeeper method
return "Hello, " + form.___["name"] # blank 2: the clean values dict
return "Please enter your name."
print(handle({"name": "Grace"}))
# EXPECTED OUTPUT:
# Hello, GraceCommon Errors (And How to Fix Them)
❌ AttributeError: 'ContactForm' object has no attribute 'cleaned_data'
You read form.cleaned_data before calling form.is_valid(), so the dictionary was never populated.
✅ Fix: only access cleaned_data inside the if form.is_valid(): block.
❌ 403 Forbidden — CSRF verification failed
Your POST template is missing the CSRF token tag.
✅ Fix: add {% csrf_token %} just inside the <form method="post"> tag.
❌ Submitted form loses its errors and entered values
On an invalid POST you created a fresh empty form instead of re-rendering the bound one.
✅ Fix: render the same bound form object — built from request.POST — so Django shows the errors and the user's input.
📋 Quick Reference
| Field / Method | What It Does |
|---|---|
| forms.CharField() | Text input; max_length, min_length, widgets |
| forms.EmailField() | Text that must be a valid email address |
| forms.IntegerField() | Converts input to an int; min_value/max_value |
| forms.BooleanField() | Checkbox; use required=False to allow unchecked |
| form.is_valid() | Runs validation; returns True and fills cleaned_data |
| form.cleaned_data | Dict of converted, trusted values (after is_valid()) |
| clean_<field>() | Custom per-field validation; must return the value |
🏆 Mini Challenge: Validate a Registration
Build a tiny form that validates a registration payload: a username of at least 3 characters and an age that is a whole number of 18 or more. Collect every error rather than stopping at the first.
# 🏆 Mini Challenge: validate username + age, collecting all errors.
class RegistrationForm:
def __init__(self, data):
self.data = data
self.cleaned_data = {}
self.errors = {}
def is_valid(self):
username = (self.data.get("username") or "").strip()
if len(username) < 3:
self.errors["username"] = "Must be at least 3 characters."
else:
self.cleaned_data["username"] = username
raw_age = self.data.get("age")
try:
age = int(raw_age)
except (TypeError, ValueError):
self.errors["age"] = "Enter a whole number."
else:
if age < 18:
self.errors["age"] = "You must be 18 or older."
else:
self.cleaned_data["age"] = age
return not self.errors
for payload in (
{"username": "ada_l", "age": "31"},
{"username": "x", "age": "twelve"},
{"username": "bob", "age": "16"},
):
form = RegistrationForm(payload)
if form.is_valid():
print("OK ->", form.cleaned_data)
else:
print("BAD ->", form.errors)
# EXPECTED: first OK, the other two show their specific errors.❓ Frequently Asked Questions
Lesson complete — you can capture and validate user input!
You now know how to define a Form subclass with typed fields and widgets, handle it in a view with the GET-vs-POST pattern, gate on is_valid(), read cleaned_data, and add validators and clean_<field> methods that raise ValidationError.
Practice quiz
What is the main advantage of a ModelForm over a plain Form?
- It is faster to render
- It generates fields from a model and can .save() to the database
- It skips validation
- It requires no template
Answer: It generates fields from a model and can .save() to the database. A ModelForm builds fields from a model and saves a record with one .save() call.
Which method must you call before reading form.cleaned_data?
- form.clean()
- form.validate()
- form.is_valid()
- form.save()
Answer: form.is_valid(). is_valid() runs validation and populates cleaned_data; reading it earlier raises AttributeError.
What does form.is_valid() populate when it succeeds?
- form.errors only
- form.data
- form.fields
- form.cleaned_data
Answer: form.cleaned_data. is_valid() fills cleaned_data with the converted Python values.
Where do you declare which model and fields a ModelForm uses?
- An inner Meta class with model and fields
- The __init__ method
- A separate forms.py decorator
- The model's Meta class
Answer: An inner Meta class with model and fields. A ModelForm uses an inner Meta class specifying model and fields.
How do you add custom validation to a single field?
- Override save()
- Define a clean_<fieldname> method
- Add a validators decorator to the view
- Override is_valid()
Answer: Define a clean_<fieldname> method. Define clean_<fieldname>, validate, and return the value so it stays in cleaned_data.
What must a clean_<fieldname> method return?
- True or False
- The field's value (cleaned or unchanged)
- An HttpResponse
- Nothing
Answer: The field's value (cleaned or unchanged). It must return the value so it remains in cleaned_data.
Which method validates across several fields at once?
- clean_all()
- validate_together()
- the form-wide clean() method
- full_clean_fields()
Answer: the form-wide clean() method. Override the form-wide clean() to validate fields that depend on each other.
What does the csrf_token template tag insert?
- A submit button
- A CSS class
- A visible error list
- A hidden input with the CSRF token
Answer: A hidden input with the CSRF token. csrf_token adds a hidden input; without it a POST form is rejected with 403.
What is raised to signal a validation failure in a clean method?
- forms.ValidationError
- ValueError
- AssertionError
- Http404
Answer: forms.ValidationError. Raise forms.ValidationError with a message to flag invalid input.
When should you use a plain Form instead of a ModelForm?
- When creating or editing a model instance
- Always
- For a contact or search form not tied to a table
- Only for admin pages
Answer: For a contact or search form not tied to a table. A plain Form suits forms that don't map to a database model, like contact or search forms.
Continue this course
- Previous: Relationships (FK, M2M)
- Next: ModelForms — Generate forms directly from your models to save and update records
- Quick reference: Django cheat sheet › Forms
Frequently asked questions
What is the difference between a Form and a ModelForm in Django?
A Form is a standalone class where you declare every field by hand and decide yourself what to do with the cleaned data — there's no automatic link to the database. A ModelForm is built from one of your models, so Django generates the fields for you from the model's columns and can save a record straight to the database with a single .save() call. Use a plain Form for things like a contact or search form that doesn't map to a table, and a ModelForm when the form's purpose is to create or edit a model instance.
Why do I have to call form.is_valid() before reading cleaned_data?
Validation is what populates cleaned_data. When you call form.is_valid(), Django runs every field's conversion and validation logic, fills the cleaned_data dictionary with the converted Python values, and collects any errors. If you read form.cleaned_data before calling is_valid(), the attribute won't exist yet and you'll get an AttributeError. Always treat is_valid() as the gatekeeper: only inside the True branch is cleaned_data safe to use.
What does the csrf_token template tag do and why is it required?
Django protects POST forms against Cross-Site Request Forgery attacks by requiring a secret token to be submitted with the form. The csrf_token template tag inserts a hidden input containing that token. If you leave it out of a POST form, Django's CSRF middleware rejects the submission with a 403 Forbidden error. Every form that uses the POST method must include it inside the form tags.
How do I add custom validation to a single field?
Define a method named clean_<fieldname> on your form class — for example clean_username. Inside it, read the field's value from self.cleaned_data, run your check, and raise forms.ValidationError with a helpful message if something is wrong. Crucially, the method must return the value (cleaned or unchanged) so it stays in cleaned_data. For validation that compares several fields at once, override the form-wide clean() method instead.