DEV Community

Cover image for Day 68 of 100 Days Of Code — Django Forms
M Saad Ahmad
M Saad Ahmad

Posted on

Day 68 of 100 Days Of Code — Django Forms

Yesterday, I learned Models and ORM and had a working app with real data from a database. But all that data was created through the admin panel. Today, for Day 68, I learned about Django Forms, letting users submit data from the browser directly. By the end, I had a form on a real page that validates input, handles errors, and saves to the database.


What is a Django Form?

A Django Form is a Python class that handles three things:

  • Rendering HTML input fields
  • Validating submitted data
  • Giving you clean, safe data to work with

You could write raw HTML forms and handle everything manually, but Django's form system saves a lot of work and handles security for you, including CSRF protection.


Two Types of Forms

Django has two kinds of forms:

  • Form — a standalone form, not tied to any model
  • ModelForm — a form generated directly from a model

In most cases, you'll use ModelForm because you're usually creating or editing database records.


Creating a Form

Standalone Form

# core/forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)
Enter fullscreen mode Exit fullscreen mode

ModelForm

# core/forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'is_published']
Enter fullscreen mode Exit fullscreen mode

The Meta class tells Django which model to use and which fields to include. Django generates the form fields automatically based on the model field types.

Use fields = '__all__' to include every field, or explicitly list the fields you want. Always prefer listing fields explicitly; never expose fields you don't intend to.


Common Form Fields

Field Use
CharField Short text input
EmailField Email input with validation
IntegerField Number input
BooleanField Checkbox
ChoiceField Dropdown select
DateField Date input
FileField File upload

Widgets

A widget controls how a field is rendered as HTML. Every field has a default widget, but you can override it:

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'is_published']
        widgets = {
            'title': forms.TextInput(attrs={'placeholder': 'Enter title'}),
            'content': forms.Textarea(attrs={'rows': 5}),
        }
Enter fullscreen mode Exit fullscreen mode

attrs lets you pass HTML attributes directly to the input element.


Handling Forms in Views

A form view handles two scenarios: a GET request (user visiting the page) and a POST request (user submitting the form).

# core/views.py
from django.shortcuts import render, redirect
from .forms import PostForm

def create_post(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('home')
    else:
        form = PostForm()

    return render(request, 'core/create_post.html', {'form': form})
Enter fullscreen mode Exit fullscreen mode

This pattern is the standard Django form view:

  1. If POST — bind the submitted data to the form with PostForm(request.POST)
  2. Validate with is_valid() — runs all field validators and form-level validation
  3. If valid — save and redirect
  4. If GET or invalid — render the form (with errors if invalid)

The redirect() after a successful save prevents the form from being resubmitted if the user refreshes the page.


Rendering the Form in a Template

<!-- core/templates/core/create_post.html -->
{% extends 'core/base.html' %}

{% block content %}
    <h1>Create Post</h1>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">Save</button>
    </form>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Three things to notice:

  • method="post" — forms that submit data must use POST
  • {% csrf_token %} — required by Django for every POST form, protects against cross-site request forgery attacks. Django will reject the form without it.
  • {{ form.as_p }} — renders all form fields wrapped in <p> tags

Other rendering options:

  • {{ form.as_p }} — fields in paragraph tags
  • {{ form.as_ul }} — fields in list items
  • {{ form.as_table }} — fields in table rows
  • {{ form }} — same as as_table

Rendering Fields Manually

For more control over layout, render fields individually:

<form method="post">
    {% csrf_token %}
    <div>
        <label for="{{ form.title.id_for_label }}">Title</label>
        {{ form.title }}
        {% if form.title.errors %}
            <span>{{ form.title.errors }}</span>
        {% endif %}
    </div>
    <div>
        <label for="{{ form.content.id_for_label }}">Content</label>
        {{ form.content }}
        {% if form.content.errors %}
            <span>{{ form.content.errors }}</span>
        {% endif %}
    </div>
    <button type="submit">Save</button>
</form>
Enter fullscreen mode Exit fullscreen mode

This gives you full control over the HTML structure while still using Django's field rendering and error handling.


Form Validation

is_valid() runs two levels of validation:

Field-level — built into each field type. EmailField checks it's a valid email, IntegerField checks it's a number, CharField checks max_length, and so on.

Custom validation — you can add your own rules:

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content']

    def clean_title(self):
        title = self.cleaned_data['title']
        if len(title) < 5:
            raise forms.ValidationError("Title must be at least 5 characters.")
        return title

    def clean(self):
        cleaned_data = super().clean()
        title = cleaned_data.get('title')
        content = cleaned_data.get('content')
        if title and content and title == content:
            raise forms.ValidationError("Title and content cannot be the same.")
        return cleaned_data
Enter fullscreen mode Exit fullscreen mode
  • clean_<fieldname>() — validates a single field
  • clean() — validates across multiple fields at once

After is_valid() passes, access the validated data through form.cleaned_data:

if form.is_valid():
    title = form.cleaned_data['title']
Enter fullscreen mode Exit fullscreen mode

Editing an Existing Object

To edit rather than create, pass the existing instance to the form:

def edit_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == 'POST':
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect('home')
    else:
        form = PostForm(instance=post)

    return render(request, 'core/edit_post.html', {'form': form})
Enter fullscreen mode Exit fullscreen mode

Passing instance=post pre-fills the form with the existing data and tells save() to update rather than create.


URL Setup

# core/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='home'),
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
    path('post/create/', views.create_post, name='create_post'),
    path('post/<int:pk>/edit/', views.edit_post, name='edit_post'),
]
Enter fullscreen mode Exit fullscreen mode

The Full Flow

User visits /post/create/
        ↓
GET request → view creates empty form
        ↓
Template renders the form as HTML
        ↓
User fills in fields and submits
        ↓
POST request → view binds data to form
        ↓
form.is_valid() runs all validators
        ↓
If valid → form.save() → redirect
If invalid → re-render form with errors shown
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

Forms are where Django starts feeling like a real full-stack framework. The model defines the data structure, the form handles input and validation, the view orchestrates the flow, and the template renders it. All four layers working together.

Tomorrow: Django Authentication; user registration, login, and logout.

Thanks for reading. Feel free to share your thoughts!

Top comments (0)