Hello! Welcome to your next lesson on building web applications with Django.
In our last lesson, you saw how Django's admin interface can automatically generate forms to create, update, and delete data from your models. This is fantastic for internal use, but it's not what you'll show to the end-users of your Task Management application.
This lesson bridges that gap. We're going to move beyond the admin panel and learn how to build our own custom forms for the main website. You will learn to create a Django Form to handle and validate user input. This skill is essential for almost any web application, as it's the primary way you'll collect information from your users, whether it's for creating a new task, signing up for an account, or submitting a contact request.
The Django Form Handling Process
Before we write any code, let's understand the life cycle of a form in a web application. Your IT background gives you a good foundation in the request-response cycle, and Django's form handling fits right into that model.
Essentially, the process involves:
- Initial Request (GET): A user navigates to a page with a form. The server sends back an empty (or "unbound") form rendered as HTML.
- Submission (POST): The user fills out the form and clicks "submit." The browser sends the data back to the server in a POST request.
- Processing: The server "binds" the submitted data to the form, cleans it (to prevent security issues), and validates it against your rules.
- Response:
- If validation fails, the server re-renders the page with the form, the user's data, and error messages.
- If validation succeeds, the server performs an action (like saving data or sending an email) and then redirects the user to a success page.
To get a solid grasp of this workflow, the MDN Web Docs provide an excellent guide.
Django Tutorial Part 9: Working with forms - MDN Web Docs
Read this section from the MDN Web Docs. It provides a clear, high-level overview and a flowchart of the entire form handling process in Django. This will give you a strong conceptual foundation before we dive into the code.
Focus on the section "Django form handling process". Pay attention to the distinction between an "unbound" form (on first visit) and a "bound" form (after submission).
Step 1: Defining a Form
In Django, you define a form's structure in a Python class. The convention is to create a forms.py file inside your app directory (e.g., alongside views.py and models.py).
A standard forms.Form class is used when you need a form that doesn't directly map to a database model. This is perfect for things like a contact form, a search form, or a feedback form.
Let's create a simple form. Inside your tasks app directory, create a new file named forms.py and add the following code:
# tasks/forms.py
from django import forms
class FeedbackForm(forms.Form):
name = forms.CharField(label='Your Name', max_length=100)
email = forms.EmailField(label='Your Email')
message = forms.CharField(label='Your Feedback', widget=forms.Textarea)
Here's what this code does:
- We import the
formsmodule. - We create a class
FeedbackFormthat inherits fromforms.Form. - We define the fields of our form:
name,email, andmessage. Each field is an instance of aFieldclass (e.g.,CharField,EmailField). These classes determine the field's validation logic and its default HTML widget. - We use arguments like
labelto set a user-friendly name andwidgetto change the HTML input type (from a single-line<input>to a multi-line<textarea>).
The MDN guide provides an excellent reference for declaring forms and understanding their fields.
Django Tutorial Part 9: Working with forms - MDN Web Docs
To understand the details of defining a form, read the "Declaring a Form" section in the MDN guide. It explains the syntax and lists the most common field types and their arguments.
Read the subsections "Declaring a Form" and "Form fields". Pay special attention to the list of common arguments like required, label, initial, and help_text.
Step 2: Rendering the Form in a Template
Now that we have a Form class, we need a view to handle it and a template to display it.
-
Update your
views.py:
Create a new function-based view to handle the feedback form.# tasks/views.py from django.shortcuts import render, redirect from .forms import FeedbackForm # Import our new form def feedback_view(request): # We'll add the form processing logic here later form = FeedbackForm() # Create an instance of the form return render(request, 'tasks/feedback.html', {'form': form}) -
Create a URL for the view:
Opentasks/urls.pyand add a path for our new view.# tasks/urls.py from django.urls import path from . import views urlpatterns = [ # ... your existing urls ... path('feedback/', views.feedback_view, name='feedback'), ] -
Create the template:
In yourtemplates/tasks/directory, create a file namedfeedback.html.<!-- templates/tasks/feedback.html --> {% extends "base_generic.html" %} <!-- Assuming you have a base template --> {% block content %} <h2>Submit Feedback</h2> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">Submit</button> </form> {% endblock %}Key elements here are:
<form method="post">: Tells the browser to send the data using the POST method, which is standard for form submissions that change data or have side-effects.{% csrf_token %}: This is a crucial security measure that Django requires to prevent Cross-Site Request Forgery attacks. Never create a POST form without it.{{ form.as_p }}: This is Django's magic for rendering the form fields. It will render each field wrapped in a<p>tag. You can also use.as_ul(for bullet points) or.as_table.
Now, start your server (python manage.py runserver) and navigate to http://127.0.0.1:8000/tasks/feedback/. You should see your form!
Step 3: Processing the Submission and Validation
Right now, submitting the form doesn't do anything. Let's add the processing logic to our view. We need to handle both the initial GET request and the subsequent POST submission.
Update your feedback_view in tasks/views.py:
# tasks/views.py
from django.shortcuts import render, redirect
from .forms import FeedbackForm
def feedback_view(request):
if request.method == 'POST':
# Create a form instance and populate it with data from the request:
form = FeedbackForm(request.POST)
# Check if the form is valid:
if form.is_valid():
# Process the data in form.cleaned_data
# For now, let's just print it to the console
print("Name:", form.cleaned_data['name'])
print("Email:", form.cleaned_data['email'])
print("Message:", form.cleaned_data['message'])
# Redirect to a new URL:
return redirect('home') # Assuming you have a 'home' URL name
else:
# If a GET (or any other method) we'll create a blank form
form = FeedbackForm()
return render(request, 'tasks/feedback.html', {'form': form})
This is the standard pattern:
if request.method == 'POST': We check if the form was submitted.form = FeedbackForm(request.POST): We create a "bound" form instance using the submitted data.if form.is_valid(): This is the most important step. It runs all validation rules for every field. If a field is invalid (e.g., an invalid email format),is_valid()returnsFalse, and theformobject is populated with error messages.form.cleaned_data: If the form is valid, the data is available in this dictionary. The data is "cleaned"—meaning it's sanitized and converted to the correct Python types. Always usecleaned_data.else: If the request is aGET, we simply create an empty, "unbound" form to display.
Step 4: Custom Validation
Django's fields handle basic validation (e.g., EmailField checks for a valid email structure). But what if you need more specific rules? You can add custom validation methods to your form class.
Let's say we want to make sure the feedback message isn't just a few words. We can add a method called clean_message to our FeedbackForm.
Update tasks/forms.py:
# tasks/forms.py
from django import forms
from django.core.exceptions import ValidationError
class FeedbackForm(forms.Form):
name = forms.CharField(label='Your Name', max_length=100)
email = forms.EmailField(label='Your Email')
message = forms.CharField(label='Your Feedback', widget=forms.Textarea)
def clean_message(self):
message = self.cleaned_data.get('message')
if len(message) < 10:
raise ValidationError("The message must be at least 10 characters long.")
return message
The pattern clean_<fieldname>() is automatically called by form.is_valid().
- We get the field's data from
self.cleaned_data. - We perform our check.
- If the check fails, we raise a
ValidationErrorwith a helpful message. - If it passes, we must return the cleaned data.
Now, if a user submits a message with fewer than 10 characters, is_valid() will return False, and our view will re-render the template. Django automatically attaches the error message to the correct field, so you'll see it displayed on the page.

Test your understanding!
Let's add another validation rule. Modify your FeedbackForm in tasks/forms.py. Add a clean_name method that raises a ValidationError if the submitted name is "Spam".
Show answer
Your tasks/forms.py file should now look something like this:
# tasks/forms.py
from django import forms
from django.core.exceptions import ValidationError
class FeedbackForm(forms.Form):
name = forms.CharField(label='Your Name', max_length=100)
email = forms.EmailField(label='Your Email')
message = forms.CharField(label='Your Feedback', widget=forms.Textarea)
def clean_message(self):
message = self.cleaned_data.get('message')
if len(message) < 10:
raise ValidationError("The message must be at least 10 characters long.")
return message
def clean_name(self):
name = self.cleaned_data.get('name')
if name.lower() == 'spam':
raise ValidationError("Please use a real name.")
return name
Try submitting the form with "Spam" or "spam" as the name. You should see your custom error message appear next to the name field.
Conclusion
Congratulations! You've just learned one of the most fundamental skills in web development: how to handle user input securely and robustly.
Key Takeaways:
- Django forms are defined as classes inheriting from
forms.Formin aforms.pyfile. - Fields like
CharFieldandEmailFielddefine the form's data types and basic validation. - Forms are rendered in templates using a
<form>tag, a{% csrf_token %}, and the{{ form }}variable. - Views handle the
GETvs.POSTlogic.is_valid()is the key method for triggering validation. - Validated data should always be accessed from
form.cleaned_data. - You can add custom logic by creating
clean_<fieldname>()methods in your form class.
Preview of the Next Lesson:
In this lesson, we built a Form from scratch, defining each field manually. This is perfect for forms that don't directly correspond to a model in your database. However, for our task manager, we'll need forms to create and edit Task objects. Redefining all the fields from our Task model (title, description, due_date, etc.) in a form would be repetitive.
In the next lesson, we'll introduce ModelForm, a powerful subclass of Form that automatically generates form fields directly from a Django model. This will simplify our code significantly and is a cornerstone of rapid development in Django.