Welcome to Part 4 of my tutorial series on building a To Do List app with Django. In the previous tutorial, we added views to process requests from the user. In this tutorial, we will create a form that will let users submit new tasks for their To Do List.
The Code
As usual, the code for this tutorial is available on Github. If you would like to follow along, the starting code is here, and the finished code is here.
Step 1: Create a form class
Django takes an object orientated approach to forms. The form classes can give you functionality including:
- Rendering entire forms in templates
- Cleaning data received in forms
- Validating forms with the
is_valid
method - Saving data to the database
We are going to use a ModelForm
to create our form.
In your todo
app, create a file called forms.py
.
Add the following code:
from django import forms
from todo.models import Task
class TaskForm(forms.ModelForm):
class Meta:
model = Task
fields = ["name"]
Step 2: Import the form into the view
We are going to put the form on the same page as the To Do list, so we are going to add the form to the index
view.
In your view, we need to:
- Import
TaskForm
fromtodo.forms
- Create an instance of the form inside the
index
view - Include the form in the
context
dictionary
My views.py
now looks like this:
# todo/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from todo.forms import TaskForm
from todo.models import Task
def index(request: HttpRequest) -> HttpResponse:
form = TaskForm()
tasks = Task.objects.all().order_by("-created")
context = {"tasks": tasks, "form": form}
return render(request, "temp_index.html", context)
Step 3: Add the form to the template
Go to temp_index.html
and add the following code snippet:
<form>
{% csrf_token %}
{{form}}
<input type="submit" value="Add">
</form>
Without having to type out <input> tags for individual fields, we have rendered our form.

However, the form doesn’t work because our view doesn’t have any code to handle submitted forms from the user.
Step 3: Edit our view to process POST requests
Here’s what we need to do to our view:
For a POST request:
- Create a form object using the POST request
- Check if the data supplied by the user is valid
- Create a task in the database using
form.save()
- Send an empty form and the list of tasks back to the user
For other requests
- Just send an empty form and a list of tasks back to the user
This is what my view now looks like:
def index(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
form = TaskForm(data=request.POST)
if form.is_valid():
form.save()
form = TaskForm()
tasks = Task.objects.all().order_by("-created")
context = {"tasks": tasks, "form": form}
return render(request, "temp_index.html", context)
In the browser, I can add new tasks to my to-do list and they appear immediately in the list.

Step 4: Test the form (bonus!)
We can add tests for our form.
We are going to test:
- Does our form contain a field for the task name?
- Is a form submitted with a task name valid?
- Is a form submitted with no name invalid?
These are my tests:
# todo/forms.py
from todo.forms import TaskForm
class TestTaskForm(TestCase):
def test_form_instance(self):
"""Test that form has name field"""
form = TaskForm()
self.assertIn("name", form.fields)
def test_is_valid(self):
form = TaskForm(data={"name": "Book dentist appointment"})
self.assertTrue(form.is_valid())
def test_empty_form_invalid(self):
form = TaskForm(data={"name": None})
self.assertFalse(form.is_valid())
Step 5: Add attributes to the form
So far, we have rendered the form but we don’t have any control in the template.
It’s very common to not see the form in the template as {{form}}
. Some developers prefer to write out the full HTML themselves and others prefer to render form fields individually. Widget Tweaks is a popular package that makes use of the form object in templates but gives you more control.
It is possible to render the form using {{form}}
and customise it.
However, the customisation must happen in forms.py
, not the template.
For example, I can change forms.py
to:
- Remove the label
- Add a placeholder
- Add an
aria-label
- Add CSS classes
We can do all of these by customising the widget used to render the name field.
class TaskForm(forms.ModelForm):
class Meta:
model = Task
fields = ["name"]
name = forms.CharField(
label=False,
widget=forms.TextInput(
attrs={
"aria-label": "name",
"placeholder": "What do you need to do?",
"class": "form-control form-control-lg inline-block",
}
),
)
I have added three CSS classes but right now our templates don’t load any CSS, we will take care of this in the next step.

Next Steps
Before we take things further, we are going to improve the UI.