Django Intermediate #1: Class-Based Views in Depth

8 min read

If you’ve finished the 7 posts of Django Basics, it’s time to step up. The intermediate series is seven posts that take the tools we only touched on in basics and go deep.

  • #1 Class-Based Views in depth ← this post
  • #2 ORM intermediate — annotate, aggregate, F/Q, prefetch_related
  • #3 Signals and Middleware
  • #4 Users/permissions — custom user model
  • #5 Messages / sessions / cookies
  • #6 Static/Media operations and storage backends
  • #7 Testing — TestCase, fixtures, pytest-django

The first topic is Class-Based Views (CBV). This post takes the function-based views (FBV) from Basics #4 and rewrites them as reusable classes.

FBV vs CBV — why classes #

Function-based views from Basics #4 are intuitive. One function takes a request and returns a response. But once you start repeating common patterns like CRUD across many models, similar code keeps piling up.

FBVCBV
DefinitionOne functionClass + methods
Learning curveVery lowA little higher
HTTP method dispatchif request.method == 'POST'def get, def post
ReuseDecorators / function decompositionInheritance / Mixin
Generic CRUDImplement yourselfBuilt-in (ListView etc.)
Code flow tracingTop-to-bottom, clearNeed to follow parent classes

The strength of CBV is that common patterns live in the parent class and you only override the differences. The downside is that flow goes through parent methods, so at first it’s hard to trace where things actually happen.

One rule: simple routes (health checks, simple redirects, small forms) → FBV, structured patterns like CRUD / list / detail → CBV is usually the answer. There’s no need to unify on one side.

CBV starting point — View #

The most basic class is django.views.View. You define get, post, put, delete methods per HTTP method.

blog/views.py — the simplest
from django.http import HttpResponse
from django.views import View

class HelloView(View):
    def get(self, request):
        return HttpResponse("Hello, GET")

    def post(self, request):
        return HttpResponse("Hello, POST")
blog/urls.py — as_view()
from django.urls import path
from .views import HelloView

urlpatterns = [
    path('hello/', HelloView.as_view(), name='hello'),
]

HelloView.as_view() converts a class into a callable view function. URL patterns always take a callable.

dispatch — the entry point for every request #

In CBV, dispatch is called once before the per-method dispatch happens. You can put common processing (auth, logging, etc.) here.

overriding dispatch
class HelloView(View):
    def dispatch(self, request, *args, **kwargs):
        print(f"method: {request.method}")
        return super().dispatch(request, *args, **kwargs)

    def get(self, request):
        return HttpResponse("GET")

TemplateView and RedirectView #

Learn the two simplest CBVs first and the pattern will click.

TemplateView — static page
from django.views.generic import TemplateView

class AboutView(TemplateView):
    template_name = "blog/about.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["page_title"] = "About Us"
        return ctx
RedirectView — redirect only
from django.views.generic import RedirectView

class GoToBlogView(RedirectView):
    pattern_name = "post_list"   # url name
    permanent = False
    query_string = True          # carry query string forward

get_context_data is where you build the template context. Almost every CBV has this method.

Generic CBV — built-in CRUD #

This is where CBV really shines. These are classes Django ships pre-built for common CRUD patterns.

ListView — list page #

Let’s rewrite the post list from Basics #4 — written there as an FBV — as a CBV.

🚫 FBV — written by hand
from django.shortcuts import render
from .models import Post

def post_list(request):
    posts = Post.objects.filter(published=True).order_by('-created_at')
    return render(request, 'blog/post_list.html', {'posts': posts})
✅ CBV — ListView
from django.views.generic import ListView
from .models import Post

class PostListView(ListView):
    model = Post
    template_name = "blog/post_list.html"
    context_object_name = "posts"
    paginate_by = 10
    ordering = ["-created_at"]

    def get_queryset(self):
        return super().get_queryset().filter(published=True)

Key class attributes:

  • model — which model’s list this is
  • template_name — if omitted, defaults to <app>/<model>_list.html
  • context_object_name — the variable name in the template (default object_list)
  • paginate_by — automatic pagination (Django passes page_obj, paginator in the context)
  • ordering — default ordering

Override get_queryset to apply additional filters. It’s also a clean place to handle search conditions from URL query strings.

handling a search query
class PostListView(ListView):
    model = Post
    paginate_by = 10

    def get_queryset(self):
        qs = super().get_queryset().filter(published=True)
        q = self.request.GET.get("q")
        if q:
            qs = qs.filter(title__icontains=q)
        return qs

DetailView — single record lookup #

DetailView
from django.views.generic import DetailView

class PostDetailView(DetailView):
    model = Post
    template_name = "blog/post_detail.html"
    context_object_name = "post"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["related"] = Post.objects.filter(
            category=self.object.category
        ).exclude(pk=self.object.pk)[:5]
        return ctx
urls.py — pk or slug
urlpatterns = [
    path('posts/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
    # or slug-based
    path('posts/<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
]

DetailView automatically looks up the object by the URL’s pk or slug. If not found, it returns 404 automatically.

CreateView, UpdateView, DeleteView #

The ModelForm pattern from Basics #6 is automated.

CreateView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .forms import PostForm

class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    template_name = "blog/post_form.html"
    success_url = reverse_lazy("post_list")

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
UpdateView
class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    template_name = "blog/post_form.html"

    def get_success_url(self):
        return reverse_lazy("post_detail", kwargs={"slug": self.object.slug})
DeleteView
class PostDeleteView(DeleteView):
    model = Post
    template_name = "blog/post_confirm_delete.html"
    success_url = reverse_lazy("post_list")

Key override points:

  • form_valid(form) — right after the form passes validation. Set author automatically, generate slug, etc.
  • form_invalid(form) — when validation fails
  • get_success_url() — redirect URL after success (method if dynamic, success_url attribute if static)
  • get_form_kwargs() — pass extra arguments to the form constructor

The reason for reverse_lazy is that the URL conf may not yet be loaded at class definition time. lazy defers reverse until call time.

FormView — forms without a model #

FormView — for things like a contact form
from django.views.generic.edit import FormView
from .forms import ContactForm

class ContactView(FormView):
    template_name = "blog/contact.html"
    form_class = ContactForm
    success_url = reverse_lazy("contact_done")

    def form_valid(self, form):
        form.send_email()
        return super().form_valid(form)

A good fit for places that need form validation + post-processing without DB persistence.

Mixin — assembling small behaviors #

The real power of CBV is Mixins. You put small behaviors (auth check, permission check, adding context, etc.) in separate classes and assemble them via multiple inheritance.

LoginRequiredMixin — login required #

login-required view
from django.contrib.auth.mixins import LoginRequiredMixin

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    login_url = "/accounts/login/"     # where to send unauthenticated users (default settings.LOGIN_URL)
    redirect_field_name = "next"

LoginRequiredMixin must come first. Multiple-inheritance MRO has to run the check logic before everything else.

PermissionRequiredMixin — permission check #

permission required
from django.contrib.auth.mixins import PermissionRequiredMixin

class PostUpdateView(PermissionRequiredMixin, UpdateView):
    model = Post
    form_class = PostForm
    permission_required = "blog.change_post"
    raise_exception = True   # without this, 403; with True, raises PermissionDenied

permission_required can be a string or a list. The permission system itself is covered in detail in #4 Users/permissions.

UserPassesTestMixin — arbitrary condition #

only the author can edit
from django.contrib.auth.mixins import UserPassesTestMixin

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    form_class = PostForm

    def test_func(self):
        post = self.get_object()
        return post.author == self.request.user

If test_func returns True, it passes. A clean fit for per-object permissions (only this post’s author can edit).

Writing your own Mixin #

shared-context Mixin
class SidebarContextMixin:
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["recent_posts"] = Post.objects.order_by("-created_at")[:5]
        ctx["popular_tags"] = Tag.objects.popular()[:10]
        return ctx

class PostListView(SidebarContextMixin, ListView):
    model = Post

class PostDetailView(SidebarContextMixin, DetailView):
    model = Post

Gather context shared by many views into one place. This is where the real benefit of class inheritance pays off.

Tracing the flow — where does what happen #

The biggest reason CBV feels hard is not knowing how methods flow through the parent classes. Let’s trace ListView’s flow once.

ListView flow (simplified)
URL → as_view() → dispatch(request)
                  get(request)
            self.object_list = self.get_queryset()
              context = self.get_context_data()
        self.render_to_response(context)

Override points:

MethodPurpose
dispatchCommon preprocessing for all methods
get_querysetModify the list queryset (filter, ordering)
get_context_dataExtra data passed to the template
get_template_namesDecide template name dynamically
form_valid/form_invalidForm processing hook (Create/Update)
get_success_urlWhere to go after success

If the flow gets confusing, the official Class-based generic views docs page or the Classy CBV site (ccbv.co.uk) is a huge help. All attributes and methods of each class are listed at a glance.

URL registration patterns #

blog/urls.py — registering CBVs
from django.urls import path
from . import views

app_name = "blog"

urlpatterns = [
    path('', views.PostListView.as_view(), name='post_list'),
    path('posts/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
    path('posts/new/', views.PostCreateView.as_view(), name='post_create'),
    path('posts/<slug:slug>/edit/', views.PostUpdateView.as_view(), name='post_update'),
    path('posts/<slug:slug>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]

as_view() is a class method — you call it without instantiating the class. A new instance is created on every URL match, so instance state is not shared between requests.

When FBV / when CBV #

SituationRecommendation
Health check, simple redirect, simple searchFBV
Small endpoint that’s just one or two APIsFBV
Standard CRUD (list/detail/create/update/delete)CBV (Generic)
Repeated cross-cutting concerns like auth/loginCBV (Mixin)
Complex flow with many branchesFBV (readability)
Same pattern across many modelsCBV (reuse)

The Django community’s usual answer: mixing the two is natural. Use CBV where CBV fits, use FBV where FBV fits.

Summary #

What we covered in this post:

  • The starting point of CBV — View + as_view() + per-method dispatch
  • dispatch is the entry point for every request
  • TemplateView, RedirectView — the shortest CBVs
  • Generic CBVListView, DetailView, CreateView, UpdateView, DeleteView, FormView
  • Key overrides: get_queryset, get_context_data, form_valid, get_success_url
  • MixinLoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin, your own context Mixin
  • Mixin inheritance order matters (the one in front runs first)
  • reverse_lazy defers URL resolution until call time
  • FBV/CBV is not either-or — mix them as appropriate

In the next post (#2 ORM intermediate), we stack annotate, aggregate, F/Q, select_related/prefetch_related — proper ORM tools — on top of the simple QuerySet from Basics #3. Including the N+1 problem and how to solve it, all in one place.

X