Overview
Django forms handle user input validation, rendering, and data processing. Fromdjango.forms.forms, the BaseForm class provides the core form functionality.
Forms bridge the gap between HTML form elements and Python data structures, providing automatic validation, security, and rendering.
Creating Forms
Basic Form
Define forms by inheriting fromdjango.forms.Form:
from django import forms
class ContactForm(forms.Form):
"""Contact form with validation"""
name = forms.CharField(
max_length=100,
label='Your Name',
help_text='Enter your full name'
)
email = forms.EmailField(
label='Email Address',
widget=forms.EmailInput(attrs={'placeholder': '[email protected]'})
)
subject = forms.CharField(max_length=200)
message = forms.CharField(
widget=forms.Textarea(attrs={'rows': 5}),
help_text='Maximum 1000 characters'
)
priority = forms.ChoiceField(
choices=[
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
],
initial='medium'
)
subscribe = forms.BooleanField(
required=False,
label='Subscribe to newsletter'
)
Model Forms
Create forms automatically from models:from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
"""Form for creating/editing articles"""
class Meta:
model = Article
fields = ['title', 'content', 'status', 'published_date']
# or exclude specific fields:
# exclude = ['created_at', 'updated_at']
labels = {
'title': 'Article Title',
'content': 'Article Content',
}
help_texts = {
'title': 'Enter a descriptive title',
'status': 'Choose publication status',
}
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'rows': 10, 'class': 'form-control'}),
'published_date': forms.DateInput(attrs={'type': 'date'}),
}
error_messages = {
'title': {
'required': 'Please provide a title',
'max_length': 'Title is too long (max 200 characters)',
},
}
ModelForms automatically generate fields based on model field types and include model validation.
Field Types
Fromdjango.forms.fields, Django provides comprehensive field types:
class CompleteForm(forms.Form):
# Text fields
name = forms.CharField(max_length=100)
email = forms.EmailField()
url = forms.URLField()
slug = forms.SlugField()
text = forms.CharField(widget=forms.Textarea)
# Numeric fields
age = forms.IntegerField(min_value=0, max_value=120)
price = forms.DecimalField(max_digits=6, decimal_places=2)
rating = forms.FloatField(min_value=0.0, max_value=5.0)
# Date/time fields
birth_date = forms.DateField()
appointment = forms.DateTimeField()
duration = forms.DurationField()
time = forms.TimeField()
# Boolean field
agree_terms = forms.BooleanField(required=True)
newsletter = forms.NullBooleanField() # True, False, or None
# Choice fields
country = forms.ChoiceField(choices=[('us', 'USA'), ('uk', 'UK')])
categories = forms.MultipleChoiceField(
choices=[('tech', 'Technology'), ('news', 'News')]
)
category = forms.TypedChoiceField(
choices=[('1', 'Tech'), ('2', 'News')],
coerce=int # Convert to integer
)
# File fields
document = forms.FileField()
image = forms.ImageField()
# Special fields
ip_address = forms.GenericIPAddressField()
uuid = forms.UUIDField()
json_data = forms.JSONField()
Field Options
Common field parameters:class ExampleForm(forms.Form):
name = forms.CharField(
max_length=100, # Maximum length
min_length=2, # Minimum length
required=True, # Is field required?
label='Your Name', # Display label
initial='', # Default value
help_text='Enter full name', # Help text
error_messages={ # Custom error messages
'required': 'Name is required',
'max_length': 'Name too long',
},
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'John Doe',
}),
validators=[ # Custom validators
validators.RegexValidator(r'^[a-zA-Z\s]+$', 'Only letters and spaces')
],
disabled=False, # Is field disabled?
localize=False, # Use localized input?
)
Form Validation
Fromdjango.forms.forms.BaseForm, forms provide multi-level validation:
Built-in Validation
class ArticleForm(forms.Form):
title = forms.CharField(
max_length=200,
min_length=5,
validators=[
validators.RegexValidator(
r'^[A-Z]',
'Title must start with uppercase letter'
)
]
)
email = forms.EmailField() # Validates email format
url = forms.URLField() # Validates URL format
age = forms.IntegerField(min_value=0, max_value=150)
Field-Level Validation
class ArticleForm(forms.Form):
title = forms.CharField(max_length=200)
content = forms.CharField(widget=forms.Textarea)
def clean_title(self):
"""Validate title field"""
title = self.cleaned_data.get('title')
if 'forbidden' in title.lower():
raise forms.ValidationError('Title contains forbidden words')
if Article.objects.filter(title=title).exists():
raise forms.ValidationError('Article with this title already exists')
# Always return cleaned data
return title.strip().title()
def clean_content(self):
"""Validate content field"""
content = self.cleaned_data.get('content')
if len(content) < 100:
raise forms.ValidationError('Content must be at least 100 characters')
return content
Form-Level Validation
class RegistrationForm(forms.Form):
password = forms.CharField(widget=forms.PasswordInput)
password_confirm = forms.CharField(widget=forms.PasswordInput)
email = forms.EmailField()
def clean(self):
"""Validate entire form"""
cleaned_data = super().clean()
password = cleaned_data.get('password')
password_confirm = cleaned_data.get('password_confirm')
# Cross-field validation
if password and password_confirm and password != password_confirm:
raise forms.ValidationError('Passwords do not match')
# Add non-field errors
email = cleaned_data.get('email')
if email and User.objects.filter(email=email).exists():
raise forms.ValidationError('Email already registered')
return cleaned_data
Always call
super().clean() when overriding the clean() method to ensure field-level validation runs first.Using Forms in Views
Function-Based Views
from django.shortcuts import render, redirect
from .forms import ContactForm
def contact_view(request):
"""Handle contact form submission"""
if request.method == 'POST':
# Bind form to POST data
form = ContactForm(request.POST, request.FILES)
if form.is_valid():
# Access cleaned data
name = form.cleaned_data['name']
email = form.cleaned_data['email']
message = form.cleaned_data['message']
# Process form data
send_email(name, email, message)
# Redirect after successful submission
return redirect('contact-success')
else:
# Display empty form
form = ContactForm()
return render(request, 'contact.html', {'form': form})
Model Forms in Views
from django.shortcuts import render, redirect, get_object_or_404
from .models import Article
from .forms import ArticleForm
def article_create(request):
"""Create new article"""
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user
article.save()
return redirect('article-detail', pk=article.pk)
else:
form = ArticleForm()
return render(request, 'article_form.html', {'form': form})
def article_edit(request, pk):
"""Edit existing article"""
article = get_object_or_404(Article, pk=pk)
if request.method == 'POST':
form = ArticleForm(request.POST, instance=article)
if form.is_valid():
form.save()
return redirect('article-detail', pk=article.pk)
else:
form = ArticleForm(instance=article)
return render(request, 'article_form.html', {'form': form, 'article': article})
Class-Based Views
from django.views.generic import FormView, CreateView, UpdateView
from django.urls import reverse_lazy
class ContactView(FormView):
"""Handle contact form"""
template_name = 'contact.html'
form_class = ContactForm
success_url = reverse_lazy('contact-success')
def form_valid(self, form):
# Process form data
name = form.cleaned_data['name']
email = form.cleaned_data['email']
send_email(name, email)
return super().form_valid(form)
class ArticleCreateView(CreateView):
"""Create article"""
model = Article
form_class = ArticleForm
template_name = 'article_form.html'
success_url = reverse_lazy('article-list')
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class ArticleUpdateView(UpdateView):
"""Update article"""
model = Article
form_class = ArticleForm
template_name = 'article_form.html'
def get_success_url(self):
return reverse_lazy('article-detail', kwargs={'pk': self.object.pk})
Rendering Forms
Template Rendering
<!-- Simple form rendering -->
<form method="post">
{% csrf_token %}
{{ form.as_p }} <!-- Render as paragraphs -->
<button type="submit">Submit</button>
</form>
<!-- Other rendering methods -->
{{ form.as_table }} <!-- Render as table rows -->
{{ form.as_ul }} <!-- Render as list items -->
{{ form.as_div }} <!-- Render as divs (Django 4.1+) -->
Manual Field Rendering
<form method="post">
{% csrf_token %}
<!-- Display non-field errors -->
{% if form.non_field_errors %}
<div class="alert alert-error">
{{ form.non_field_errors }}
</div>
{% endif %}
<!-- Render individual fields -->
<div class="form-group">
{{ form.name.label_tag }}
{{ form.name }}
{% if form.name.help_text %}
<small>{{ form.name.help_text }}</small>
{% endif %}
{% if form.name.errors %}
<div class="error">{{ form.name.errors }}</div>
{% endif %}
</div>
<div class="form-group">
{{ form.email.label_tag }}
{{ form.email }}
{{ form.email.errors }}
</div>
<button type="submit" class="btn">Submit</button>
</form>
Field Attributes
<!-- Access field properties -->
{{ field.label }} <!-- Field label -->
{{ field.id_for_label }} <!-- HTML id attribute -->
{{ field.value }} <!-- Current value -->
{{ field.html_name }} <!-- HTML name attribute -->
{{ field.help_text }} <!-- Help text -->
{{ field.errors }} <!-- Field errors -->
{{ field.is_hidden }} <!-- Is hidden field? -->
Form Widgets
Widgets control how form fields are rendered:from django import forms
class ArticleForm(forms.Form):
# Text inputs
title = forms.CharField(widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter title',
}))
# Textarea
content = forms.CharField(widget=forms.Textarea(attrs={
'rows': 10,
'cols': 80,
}))
# Password input
password = forms.CharField(widget=forms.PasswordInput)
# Hidden input
user_id = forms.IntegerField(widget=forms.HiddenInput)
# Select dropdown
category = forms.ChoiceField(
choices=[('tech', 'Technology'), ('news', 'News')],
widget=forms.Select(attrs={'class': 'form-select'})
)
# Radio buttons
priority = forms.ChoiceField(
choices=[('low', 'Low'), ('high', 'High')],
widget=forms.RadioSelect
)
# Checkboxes
tags = forms.MultipleChoiceField(
choices=[('python', 'Python'), ('django', 'Django')],
widget=forms.CheckboxSelectMultiple
)
# Date picker
published_date = forms.DateField(widget=forms.DateInput(attrs={
'type': 'date',
}))
# File upload
document = forms.FileField(widget=forms.FileInput(attrs={
'accept': '.pdf,.doc,.docx',
}))
Formsets
Handle multiple forms at once:from django.forms import formset_factory, modelformset_factory
from .forms import ArticleForm
# Basic formset
ArticleFormSet = formset_factory(ArticleForm, extra=3, max_num=10)
def manage_articles(request):
if request.method == 'POST':
formset = ArticleFormSet(request.POST)
if formset.is_valid():
for form in formset:
if form.cleaned_data:
# Process each form
title = form.cleaned_data['title']
# Save data
return redirect('success')
else:
formset = ArticleFormSet()
return render(request, 'formset.html', {'formset': formset})
# Model formset
ArticleFormSet = modelformset_factory(
Article,
fields=['title', 'content'],
extra=1,
can_delete=True
)
def edit_articles(request):
if request.method == 'POST':
formset = ArticleFormSet(request.POST)
if formset.is_valid():
formset.save()
return redirect('article-list')
else:
formset = ArticleFormSet(queryset=Article.objects.all())
return render(request, 'formset.html', {'formset': formset})
<!-- Render formset -->
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<div class="formset-form">
{{ form.as_p }}
</div>
{% endfor %}
<button type="submit">Save All</button>
</form>
Always include
{{ formset.management_form }} when rendering formsets. It contains hidden fields Django needs to track forms.Best Practices
- Use ModelForms: Leverage ModelForms for model-backed forms
- Validate properly: Implement field and form-level validation
- Handle errors: Display validation errors to users
- Use CSRF protection: Always include
{% csrf_token %} - Redirect after POST: Use redirect after successful form submission
- Clean data: Always use
cleaned_dataafter validation
Never trust user input. Always validate form data before processing or saving to the database.