Download Free Install Free

Django Forms: Working with Forms in Python

Damian Hites
April 16, 2019

Table of Contents

Why use Django Forms?

Dealing with HTML forms in a web application can be a complicated task: ideally, you’d have a standard way of rendering input fields and processing the inputted data. Django forms provide you with a framework that does just that. Django comes with some standard ways of rendering forms with inputs of various types; it does field validation and takes care of some of the security concerns you would normally need to figure out for yourself. By using Django’s forms, you can avoid reinventing the wheel and get guided into some good practices that will help you avoid having to write a lot of repetitive code or create your own framework. In this article, we will take a look at how to get started with Django Forms and hopefully give you a sense of how to use them to cover your needs.

A Simple Example

For demonstration purposes, let’s say we’re building a web application for tracking motor vehicles and want to create a form for inputting the Make/Model/Year of a vehicle. In this example, we’ll want to define the form, display the form and then process the posted data. Let’s start by defining the form in question. It’s a good idea to keep your forms separate from your views, so in a file called forms.py:

from django import forms

class VehicleForm(forms.Form):
make = forms.CharField()
model = forms.CharField()
year = forms.IntegerField()

This defines a form that has three fields: The Make of the vehicle, the Modelof the vehicle and the Year of the vehicle. The make and model fields expect text input. The year field expects an integer as input. To surface the form to the user, Django has the convenient FormView class:

from django.urls import reverse_lazy
from django.views.generic.edit import FormView

from .forms import VehicleForm


class VehicleView(FormView):
form_class = VehicleForm
template_name = 'vehicle.html'
success_url = reverse_lazy('success')

The view specifies that the form we’ll be using to render and process the data will be the VehicleForm – the template for form rendering will be vehicle.html and after a successful form submission, it will redirect to the view named success. For rendering the form, let’s give a bare-bones template that simply renders the form with a submit button that will post the form to our VehicleView:

<!DOCTYPE html>
<html>
  <head>
    <title>Vehicle</title>
  </head>
  <body>
    <form method="post">
      {% csrf_token %}
      {{ form.as_ul }}
      <button type="submit">Submit</button>
    </form>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <title>Vehicle</title>
  </head>
  <body>
    <form method="post">
      {% csrf_token %}
      {{ form.as_ul }}
      <button type="submit">Submit</button>
    </form>
  </body>
</html>

The template defines an HTML form with a submit button and uses our VehicleForm class to render the form fields as HTML (in this case we have indicated we want the form rendered as an unordered list). Additionally, the template uses Django’s built-in csrf_token template tag to render the CSRF token as a part of the form. CSRF protection is built-in to Django’s forms, and if you omit that line you’ll get an error when trying to submit the form. This is a great security feature that you get pretty much for free. Finally, if we define a success view and submit our form, we should see that we get redirected to our success view.

Customizing Forms

The proposed solution above is a great start. With very little code, it provides the start of a very simple solution that is both functional and secure. If you open your browser and go to the page that corresponds to your form view, you’ll see a page that has no styling and renders with some basic HTML input fields that don’t constrain inputs to valid values. While the form handles some validation in the view, it allows invalid values for our use case. Setting aside the conversation about styling that is really separate from Django’s form rendering, let’s take a look at how we might customize our form to make it a bit more robust.

Custom Rendering

When it comes to actually controlling how a form renders, Django gives a number of options from manually writing the HTML form into your template to using other predefined or custom widgets with their own templates.

Widgets

A field on a Django form defines how the data should be validated and processed. The corresponding widget defines what HTML elements are used to render that field. To illustrate how you can use widgets, let’s continue with our example above The year field on the form is rendered as an HTML number input element, but really we only want to allow the user to specify valid years. One way to enforce this is to have a dropdown containing the set of possible years. To do this, we can use Django’s built-in Select widget:

from django import forms

class VehicleForm(forms.Form):
make = forms.CharField()
model = forms.CharField()
year = forms.IntegerField(
widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
)

The snippet above changes the widget that the IntegerField is using to a Select widget which uses an HTML select element to render a list of options. The widget accepts a list of tuples to indicate the valid choices, so we pass a list of numbers between the earliest year a VIN can represent to the current year. With these changes, you can see that the year field now renders the list of options.

Templates

When you want to change the appearance of existing widgets or want to customize how a specific form renders, you can always completely override default behavior. You can do so either by overriding the default Django form field templates, or by manually crafting the HTML form in your page template – bypassing the form’s default rendering mechanisms. I won’t be covering Django templates in this post, however, as they are a bit of a different topic.

Custom Validation & Data Manipulation

So far in our example we’ve shown how you can customize a field to limit the available options to the allowable values. This doesn’t actually stop somebody from POSTing to your endpoint with invalid data, so it’s still important to do data validation within your form. There are a number of places that Django recommends you do validation, but at the form level, you can either use validators or you can include validation in the clean methods: clean or clean_<field_name>.

Validators

Validators are functions that take a value, raise a ValueError exception on failed validation and None on success. Continuing with our example above, we can add validation to the year field to prevent invalid years being inputted.

from django import forms

def validate_year(year):
if year < 1981 or year > 2019:
raise forms.ValidationError('Not a valid year for a VIN')

class VehicleForm(forms.Form):
make = forms.CharField()
model = forms.CharField()
year = forms.IntegerField(
validators=[validate_year],
widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
)

Now when the VehicleForm is validating the data, it will run our validate_year function to determine that we have a valid year.

Clean Methods

To allow for further customization of forms, Django will call clean methods if they are defined on your form. To do additional validation on a field in a form, you have the option to implement a clean_<field_name> method that can be used to “clean” the data. By way of example, let’s consider our make and model fields. Let’s say we want to support a specific number of car Makes and Models and we want to make sure that the data that comes out of the form is in title case format. We can ensure this in the clean methods:

from django import forms
from django.template.defaultfilters import title

def validate_year(year):
if year < 1981 or year > 2019:
raise forms.ValidationError('Not a valid year for a VIN')

class VehicleForm(forms.Form):
make = forms.CharField()
model = forms.CharField()
year = forms.IntegerField(
validators=[validate_year],
widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
)

def clean_make(self):
make = self.cleaned_data['make']
if make.lower() not in {'chevrolet', 'ford'}:
raise forms.ValidationError('Unrecognized Make')
return title(make)

def clean_model(self):
model = self.cleaned_data['model']
if model.lower() not in {'el camino', 'mustang'}:
raise forms.ValidationError('Unrecognized Model')
return title(model)

The methods clean_make and clean_model make sure that the processed values in the form are in title case (first letter of each word capitalized) but also do some validation by making sure they have correct values. Now let’s take a look at a specific example of a car, the El Camino. This is a car that only had models until 1987. In this case we have validation logic that needs information from multiple fields. We can use the clean method to do this sort of validation, like so:

from django import forms
from django.template.defaultfilters import title

def validate_year(year):
if year < 1981 or year > 2019:
raise forms.ValidationError('Not a valid year for a VIN')

class VehicleForm(forms.Form):
make = forms.CharField()
model = forms.CharField()
year = forms.IntegerField(
validators=[validate_year],
widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
)

def clean_make(self):
make = self.cleaned_data['make']
if make.lower() not in {'chevrolet', 'ford'}:
raise forms.ValidationError('Unrecognized Make')
return title(make)

def clean_model(self):
model = self.cleaned_data['model']
if model.lower() not in {'el camino', 'mustang'}:
raise forms.ValidationError('Unrecognized Model')
return title(model)

def clean(self):
cleaned_data = super().clean()
make = cleaned_data.get('make')
model = cleaned_data.get('model')
year = cleaned_data.get('year')

if model and model.lower() == 'el camino':
if make and make.lower() != 'chevrolet':
raise forms.ValidationError('Make & Model do not match!')
if year and year > 1987:
raise forms.ValidationError(
'This Make & Model was not produced in provided year'
)

In our clean method, we can validate that if we are specifying an El Camino, the other values have to match so that you don’t accept invalid input. If you specify an El Camino, it better be a Chevrolet made between 1981 and 1987.

Custom Form Fields

If you have a field with custom logic on a form that you want to use again and again, you can make a custom form field. Form fields are quite flexible and a more complicated topic, so for the purposes of this article, let’s take a look at a simple example of replacing some of our custom logic with a custom form field to reduce duplicated code. In our previous example, the clean_make and clean_model methods are very similar, so let’s see if we can reduce code duplication by creating a custom form field:

from django import forms
from django.template.defaultfilters import title

def validate_year(year):
if year < 1981 or year > 2019:
raise forms.ValidationError('Not a valid year for a VIN')

class TitleChoiceField(forms.CharField):
def __init__(self, *args, **kwargs):
self.choices = kwargs.pop('choices', None)
super(TitleChoiceField, self).__init__(*args, **kwargs)

def clean(self, value):
if value.lower() not in self.choices:
raise forms.ValidationError('Invalid value. Must be one of {}'.format(self.choices))
return title(value)

class VehicleForm(forms.Form):
make = TitleChoiceField(choices={'chevrolet', 'ford'})
model = TitleChoiceField(choices={'el camino', 'mustang'})
year = forms.IntegerField(
validators=[validate_year],
widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
)

def clean(self):
cleaned_data = super().clean()
make = cleaned_data.get('make')
model =cleaned_data.get('model')
year = cleaned_data.get('year')

if model and model.lower() == 'el camino':
if make and make.lower() != 'chevrolet':
raise forms.ValidationError('Make & Model do not match!')
if year and year > 1987:
raise forms.ValidationError('This Make & Model was not produced in provided year')

The make and model fields have similar functionalities. By defining a custom form field that takes care of the similar logic, we’re able to reduce the amount of repetitive code and standardize across our application how to handle a specific type of field. Ultimately, if required, you could also specify your own widget as well to further customize how the field renders.

Using the ModelForm

Typically in Django, if you are creating a form, you will want to persist the data that is submitted to that form in some way. In many cases a form will have fields that correspond directly to one of your Django ORM models. This is a common enough pattern that there is a special type of form for it called the ModelForm. Using the ModelForm makes it simple to define forms that are based on your models and have the additional ability of saving the data using the specified model. To illustrate how this might look, let’s take our previous example with a new approach and start by defining some Django ORM models:

from django.db import models

class Make(models.Model):
name = models.CharField(max_length=128)

def __str__(self):
return self.name

class Model(models.Model):
name = models.CharField(max_length=128)

def __str__(self):
return self.name


class Vehicle(models.Model):
make = models.ForeignKey(
'Make', on_delete=models.CASCADE, related_name='vehicles'
)
model = models.ForeignKey(
'Model', on_delete=models.CASCADE, related_name='vehicles'
)
year = models.IntegerField(db_index=True)

With this model for persisting a Vehicle, it allows us to predefine makes and models that we want to support. The example is intentionally kept simple, but you could create relationships that help further define the constraints. For example, you could add a relationship between the Make and Model models to constrain certain Models to a certain Make. Assuming this simple relationship, however, here we have a possible resulting form for matching our previous example:

from django import forms
from django.template.defaultfilters import title

from .models import Vehicle

def validate_year(year):
if year < 1981 or year > 2019:
raise forms.ValidationError('Not a valid year for a VIN')


class VehicleForm(forms.ModelForm):
class Meta:
model = Vehicle
fields = ('make', 'model', 'year')

year = forms.IntegerField(
validators=[validate_year],
widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
)

As you can see, the definition of the form is a bit more concise. You can simply indicate which fields to include from the model – and, as long as you are happy with the defaults – it just works. For fields where you need more customization, like the year field in this example, you can still specify the field to override the default declaration. If you take our example and you change the VehicleView to inherit from Django’s CreateView, the VehicleForm‘s save method will be called when the form is submitted and it will now be automatically persisted.

Customizing the ModelForm

A ModelForm is just a Form, so any customization that applies to a regular Form also applies to a ModelForm. Additionally, you can customize the process of saving the data from the form to the database. To customize this process, you can override the save method. As an example, let’s assume that we want to automatically fill in the make of a vehicle based on a selected vehicle model to avoid the situation where somebody might specify mismatching makes and models:

from django import forms
from django.template.defaultfilters import title

from vehicles.models import Vehicle


def validate_year(year):
if year < 1981 or year > 2019:
raise forms.ValidationError('Not a valid year for a VIN')


class VehicleForm(forms.ModelForm):
class Meta:
model = Vehicle
fields = ('model', 'year')

year = forms.IntegerField(
validators=[validate_year],
widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
)

_model_make_map = {
'El Camino': 'Chevrolet',
'Mustang': 'Ford',
}

def save(self, commit=True):
instance = super(VehicleForm, self).save(commit=False)
make_name = self._model_make_map[instance.model.name]
make = Make.objects.get(name=make_name)
instance.make = make
if commit:
instance.save()
return instance

When a specific model is selected in this example, the corresponding make of the vehicle is selected based on the mapping that we have. The option to even specify the make of the vehicle separately from the model has been removed. Once you select the model, the make is automatically determined and prepopulated.

Conclusion

Django forms have significant provided functionality that can get you going very quickly – particularly if you generate them from your models by using the ModelForm. If you need custom logic in your form, there are a number of places where you can customize behavior including adding your own form fields and widgets to standardize behavior across your application. Forms are clearly a very powerful tool that Django provides for dealing the HTML forms in a standardized and secure way!