Difficulty: Intermediate
Django's form system handles three primary tasks: rendering HTML form inputs, validating submitted data, and converting submitted strings to Python types. Forms provide a declarative way to define form fields, their validation rules, and their HTML representation.
A Django Form class inherits from django.forms.Form. Each class attribute is a form field that maps to an HTML input element. CharField renders as <input type="text">, EmailField as <input type="email">, IntegerField as <input type="number">, BooleanField as <input type="checkbox">, ChoiceField as <select>, and so on. Each field type includes built-in validation - EmailField validates email format, IntegerField ensures the value is a valid integer.
Form fields accept keyword arguments that control behavior. 'required' (default True) determines whether the field must be filled. 'label' customizes the label text. 'initial' sets the default value displayed in the form. 'help_text' adds descriptive text below the field. 'widget' customizes the HTML element used to render the field. 'error_messages' customizes validation error messages.
Widgets control the HTML rendering of form fields. The default widget for CharField is TextInput, but you can change it to Textarea, PasswordInput, HiddenInput, or any custom widget. Widgets accept an 'attrs' dictionary to add HTML attributes like class, placeholder, id, and data attributes. This is how you integrate Django forms with CSS frameworks.
Forms are rendered in templates using several methods. form.as_p wraps each field in <p> tags. form.as_div wraps in <div> tags. form.as_table wraps in <tr> tags. For full control, you can iterate over fields manually: {% for field in form %} gives you access to field.label_tag, field, field.errors, and field.help_text. Manual rendering is preferred for custom-designed forms.
When a form is instantiated with POST data (form = MyForm(request.POST)), calling form.is_valid() triggers validation. Django first validates each field individually (checking required, data type, max_length, etc.), then runs any custom clean_fieldname() methods, and finally runs the form-level clean() method. If validation passes, cleaned data is available in form.cleaned_data as a dictionary of Python objects.
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(
max_length=100,
label='Your Name',
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter your name',
}),
)
email = forms.EmailField(
label='Email Address',
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'you@example.com',
}),
)
subject = forms.CharField(max_length=200)
message = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
}),
help_text='Minimum 20 characters',
)
priority = forms.ChoiceField(
choices=[
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
],
initial='medium',
)
Form fields define the type, validation rules, and HTML rendering. Widgets customize the HTML elements. The attrs dictionary adds HTML attributes for CSS integration. initial sets the default selected value.
from django.shortcuts import render, redirect
from django.core.mail import send_mail
from .forms import ContactForm
def contact_view(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Access cleaned data
name = form.cleaned_data['name']
email = form.cleaned_data['email']
message = form.cleaned_data['message']
send_mail(
subject=f'Contact from {name}',
message=message,
from_email=email,
recipient_list=['admin@example.com'],
)
return redirect('contact-success')
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})
On POST, the form is bound to submitted data. is_valid() runs all validation. If valid, cleaned_data contains Python objects. If invalid, the form re-renders with error messages automatically included.
<!-- contact.html -->
<form method="post">
{% csrf_token %}
<!-- Display non-field errors -->
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<!-- Render each field manually -->
{% for field in form %}
<div class="form-group{% if field.errors %} has-error{% endif %}">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
{% endfor %}
<button type="submit">Send Message</button>
</form>
Manual rendering gives full control over HTML structure. Iterate over fields with {% for field in form %}. Access label_tag, the widget, help_text, and errors for each field. Check form.non_field_errors for form-level validation errors.
from django import forms
class RegistrationForm(forms.Form):
# Text inputs
username = forms.CharField(max_length=30)
password = forms.CharField(widget=forms.PasswordInput)
bio = forms.CharField(widget=forms.Textarea, required=False)
# Email and URL
email = forms.EmailField()
website = forms.URLField(required=False)
# Numbers
age = forms.IntegerField(min_value=13, max_value=120)
salary = forms.DecimalField(max_digits=10, decimal_places=2, required=False)
# Dates
birthdate = forms.DateField(
widget=forms.DateInput(attrs={'type': 'date'})
)
# Selection
country = forms.ChoiceField(choices=[('us', 'USA'), ('uk', 'UK')])
interests = forms.MultipleChoiceField(
choices=[('py', 'Python'), ('js', 'JavaScript'), ('go', 'Go')],
widget=forms.CheckboxSelectMultiple,
)
# Boolean
agree_tos = forms.BooleanField(label='I agree to the Terms of Service')
# File
avatar = forms.ImageField(required=False)
Django provides field types for all common HTML inputs. Widgets customize rendering: PasswordInput hides text, DateInput with type='date' shows a date picker, CheckboxSelectMultiple renders checkboxes instead of a select dropdown.
Form, Fields, Widgets, Rendering, Validation