Skip to main content

Overview

Django forms handle user input validation, rendering, and data processing. From django.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 from django.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

From django.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

From django.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

  1. Use ModelForms: Leverage ModelForms for model-backed forms
  2. Validate properly: Implement field and form-level validation
  3. Handle errors: Display validation errors to users
  4. Use CSRF protection: Always include {% csrf_token %}
  5. Redirect after POST: Use redirect after successful form submission
  6. Clean data: Always use cleaned_data after validation
Never trust user input. Always validate form data before processing or saving to the database.

Next Steps

  • Learn about Views for form handling
  • Explore Models for ModelForm integration
  • Understand Templates for form rendering

Build docs developers (and LLMs) love