Skip to main content

Getting Started with Forms

In this tutorial, you will build a functional contact form using the Django forms system. You will learn how to define fields, validate user input, and handle the form lifecycle from raw data to cleaned Python objects.

What You Will Build

You will create a ContactForm that collects a user's email, a subject, and a message. By the end, you will be able to:

  1. Define a form using declarative syntax.
  2. Bind data to the form and trigger validation.
  3. Implement custom field-level and form-level validation.
  4. Render the form into HTML.

Prerequisites

To follow this tutorial, you should have a Django project environment ready. The core classes we will use are located in django/forms/forms.py and django/forms/fields.py.

Step 1: Define the Form Class

Django forms are defined as Python classes that inherit from django.forms.forms.Form. You define the form's fields as class attributes using various Field subclasses.

Create a new file (e.g., forms.py) and add the following code:

from django.forms import Form, CharField, EmailField, ValidationError
from django.forms.widgets import Textarea

class ContactForm(Form):
subject = CharField(max_length=100)
message = CharField(widget=Textarea)
sender = EmailField()
cc_myself = CharField(required=False)

In this snippet:

  • CharField and EmailField are subclasses of Field (from django.forms.fields).
  • The widget argument (like Textarea) determines how the field is rendered in HTML.
  • required=False allows the cc_myself field to be empty.

Step 2: Instantiate and Bind Data

A form instance is either unbound (no data) or bound (associated with a dictionary of data). When you pass a dictionary to the constructor, the BaseForm.__init__ method sets self.is_bound to True.

# An unbound form (used for displaying an empty form)
unbound_form = ContactForm()

# A bound form (used for processing submitted data)
data = {
'subject': 'Hello',
'message': 'This is a test message.',
'sender': 'invalid-email', # This will cause a validation error
'cc_myself': 'on'
}
bound_form = ContactForm(data)

Step 3: Validate the Form

To check if the submitted data is valid, call the is_valid() method. This method triggers the full_clean() process defined in BaseForm, which runs each field's clean() method.

if bound_form.is_valid():
# Access the cleaned, validated data
subject = bound_form.cleaned_data['subject']
print(f"Valid subject: {subject}")
else:
# Access the error dictionary
print(bound_form.errors)
# Output: {'sender': ['Enter a valid email address.']}

When is_valid() returns True, the cleaned_data attribute contains a dictionary of the validated data converted to Python objects (e.g., an EmailField ensures the string is a valid email format).

Step 4: Add Custom Validation

Sometimes you need logic that goes beyond built-in field validation. You can add methods to your form class to perform custom checks.

Field-specific validation

To validate a specific field, define a method named clean_<fieldname>().

class ContactForm(Form):
# ... (fields from Step 1)

def clean_subject(self):
data = self.cleaned_data['subject']
if "urgent" not in data.lower():
raise ValidationError("All subjects must contain the word 'urgent'!")
return data

Form-wide validation

To perform validation that involves multiple fields, override the clean() method.

class ContactForm(Form):
# ...

def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")

if cc_myself and subject and "help" not in subject:
raise ValidationError(
"You can only CC yourself if the subject contains 'help'."
)
return cleaned_data

Step 5: Render the Form

Django provides several ways to render a form into HTML. The BaseForm class includes mixins that allow you to output the form in different formats.

In your template (or for testing in a shell):

form = ContactForm()
print(form.as_p())

The as_p() method wraps each field in a <p> tag. Other options include as_ul() and as_table(). Each field is rendered as a BoundField, which combines the Field logic with the actual data and errors.

Complete Working Result

Here is your final ContactForm with custom validation:

from django.forms import Form, CharField, EmailField, ValidationError
from django.forms.widgets import Textarea

class ContactForm(Form):
subject = CharField(max_length=100)
message = CharField(widget=Textarea)
sender = EmailField()
cc_myself = CharField(required=False)

def clean_subject(self):
subject = self.cleaned_data.get('subject')
if len(subject) < 5:
raise ValidationError("Subject is too short.")
return subject

def clean(self):
cleaned_data = super().clean()
sender = cleaned_data.get('sender')
if sender and sender.endswith("@blocked.com"):
raise ValidationError("This domain is blocked.")
return cleaned_data

# Usage example:
f = ContactForm({'subject': 'Hi', 'message': 'Test', 'sender': 'user@blocked.com'})
if not f.is_valid():
print(f.errors.as_json())

Next Steps

Now that you can create and validate basic forms, explore ModelForms in django/forms/models.py to automatically generate forms from your database models, or look into Formsets to handle multiple forms on a single page.