Skip to main content

๐Ÿ—๏ธ Class-Based Views

Most web pages fall into a handful of shapes: show a list, show one object, create, edit, delete. Class-based views (CBVs) capture those shapes as reusable classes so you write only the parts that differ. In this lesson you'll learn how CBVs dispatch requests, how the generic views work, and how mixins let you compose behaviour cleanly.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Explain how as_view() and dispatch() route a request to get(), post(), etc.
  • Build read pages with ListView and DetailView, customising get_queryset and get_context_data
  • Build write pages with CreateView, UpdateView, and DeleteView
  • Compose behaviour with mixins and reason about method resolution order (MRO)
  • Apply decorators to CBVs and decide when a CBV beats a function-based view

Estimated Time: 40โ€“50 minutes  โ€ข  Difficulty: Intermediate

Hands-on: Convert a function-based blog list-and-create pair into generic CBVs.

In This Lesson

Why Class-Based Views?

In the previous lesson you wrote views as functions โ€” total control, everything visible in one place. The cost is repetition: every list page repeats "query, paginate, render," every edit page repeats "show form, validate, save, redirect." Class-based views trade a little visibility for a lot of reuse by expressing a view as a class whose methods you override only where your needs differ from the defaults.

๐Ÿ’ก A useful analogy: A function-based view is a house built from scratch โ€” you place every brick. A class-based view is a prefabricated module: the walls, wiring, and plumbing are standardised, and you customise the finishes. You get a working structure quickly and only do bespoke work where it matters.

The same "Hello, World" as a class looks like this โ€” one method per HTTP verb:

from django.views import View
from django.http import HttpResponse

class HelloWorldView(View):
    def get(self, request, *args, **kwargs):
        return HttpResponse("Hello, World!")

    def post(self, request, *args, **kwargs):
        return HttpResponse("Thanks for posting!")

๐Ÿ“– Key Terms

Generic view: a ready-made CBV (like ListView) that implements a common pattern so you configure rather than code it.

Mixin: a small class that adds one capability (e.g. login enforcement) and is combined with a view via multiple inheritance.

MRO: method resolution order โ€” the sequence Python searches parent classes to find a method.

as_view() and dispatch()

A URL pattern can't point at a class directly โ€” it needs a callable. The as_view() classmethod builds that callable. When a request arrives, as_view() instantiates the class and calls its dispatch() method, which inspects request.method and routes to the matching handler (get, post, put, deleteโ€ฆ). If no handler exists for that verb, Django returns 405 Method Not Allowed automatically.

# urls.py
from django.urls import path
from .views import HelloWorldView

urlpatterns = [
    path("hello/", HelloWorldView.as_view(), name="hello"),
]
flowchart TD A[Request] --> B["URLconf calls as_view()"] B --> C["dispatch()"] C --> D{HTTP method?} D -- GET --> E["get()"] D -- POST --> F["post()"] D -- other --> G[405 Not Allowed] E --> H[HttpResponse] F --> H

Because dispatch() is the single entry point for every method, it's the natural place to hook cross-cutting concerns โ€” which is exactly how authentication mixins and method_decorator work, as you'll see later.

The CBV Hierarchy

Django's generic views are built by layering small pieces on top of the base View class. Understanding the family tree tells you which method to override and where a default comes from.

The Django class-based view hierarchy The base View class leads to TemplateView, RedirectView, and, through single- and multiple-object mixins, to DetailView, ListView, and the editing views CreateView, UpdateView, and DeleteView. View TemplateView RedirectView Single / Multiple ObjectMixin DetailView ListView CreateView ยท UpdateView ยท DeleteView
Figure 1 โ€” Everything descends from View. The mixins carry the reusable behaviour; the leaf classes you actually subclass just wire the mixins together.

TemplateView is the simplest useful generic โ€” render a template with some context and nothing else:

from django.views.generic import TemplateView
from .models import Post

class HomeView(TemplateView):
    template_name = "blog/home.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["featured"] = Post.objects.filter(featured=True)[:3]
        return context

Display Views: List & Detail

ListView queries a model, paginates it, and renders a template โ€” the FBV post_list from the previous lesson shrinks to a handful of attributes:

from django.views.generic import ListView
from .models import Post

class PostListView(ListView):
    model = Post
    template_name = "blog/post_list.html"     # default: blog/post_list.html
    context_object_name = "posts"             # default: object_list
    paginate_by = 10

    def get_queryset(self):
        return (
            Post.objects.filter(status="published")
            .select_related("author")
            .order_by("-published_date")
        )

Override get_queryset() to control which rows appear, and get_context_data() to add extra data alongside them. DetailView is the single-object counterpart; it looks up the object by pk or slug from the URL:

from django.views.generic import DetailView
from django.db.models import F
from .models import Post

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

    def get_object(self, queryset=None):
        obj = super().get_object(queryset=queryset)
        # Atomically bump the view counter without a race condition
        Post.objects.filter(pk=obj.pk).update(view_count=F("view_count") + 1)
        return obj

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["comments"] = self.object.comments.filter(approved=True)
        return context

๐Ÿ’ก Sensible defaults you can rely on

With just model = Post, ListView looks for blog/post_list.html and exposes object_list; DetailView looks for blog/post_detail.html and exposes object. Setting context_object_name gives the variable a friendlier name in the template.

Wire them up exactly like function views, but call as_view():

urlpatterns = [
    path("", PostListView.as_view(), name="post_list"),
    path("post/<int:pk>/", PostDetailView.as_view(), name="post_detail"),
]

Editing Views: Create, Update, Delete

The editing generics build a ModelForm for you, render it, validate the submission, save, and redirect โ€” the whole GET/POST dance from the last lesson, handled internally.

from django.views.generic import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Post

class PostCreateView(CreateView):
    model = Post
    fields = ["title", "body", "status"]      # or use form_class = PostForm
    template_name = "blog/post_form.html"

    def form_valid(self, form):
        form.instance.author = self.request.user   # stamp the author
        return super().form_valid(form)

class PostUpdateView(UpdateView):
    model = Post
    fields = ["title", "body", "status"]
    template_name = "blog/post_form.html"

    def get_success_url(self):
        return reverse_lazy("blog:post_detail", kwargs={"pk": self.object.pk})

class PostDeleteView(DeleteView):
    model = Post
    template_name = "blog/post_confirm_delete.html"
    success_url = reverse_lazy("blog:post_list")

โš ๏ธ reverse_lazy, not reverse

success_url is evaluated when the class is defined, before the URL config is fully loaded. Using reverse() there raises an error; reverse_lazy() defers the lookup until it's actually needed. When the URL depends on the saved object, override get_success_url() instead so you can read self.object.

CreateView and UpdateView share one template (both render a form); by default it's <app>/<model>_form.html. DeleteView shows a confirmation page on GET and performs the delete on POST.

Mixins & Method Resolution Order

Mixins add one concern each and are combined through multiple inheritance. Django ships several for access control:

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import UpdateView
from .models import Post

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    fields = ["title", "body", "status"]

    def test_func(self):
        # Only the author may edit their own post
        return self.get_object().author == self.request.user

You can also write your own mixin to share behaviour across views:

from django.contrib import messages

class SuccessMessageMixin:
    """Flash a message after a successful form submission."""
    success_message = ""

    def form_valid(self, form):
        response = super().form_valid(form)
        if self.success_message:
            messages.success(self.request, self.success_message)
        return response

class PostCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
    model = Post
    fields = ["title", "body"]
    success_message = "Post created successfully!"

โœ… The order rule

Python resolves methods left to right, so mixins go on the left and the base generic view goes on the right. Each mixin's super().form_valid() then passes control rightward down the chain. Reverse the order and the view's own form_valid can shadow your mixin โ€” a classic silent bug.

Decorating a class-based view

Function decorators don't apply directly to methods, so wrap them with method_decorator, usually targeting dispatch so the decorator covers every HTTP verb:

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import ListView

@method_decorator(cache_page(60 * 15), name="dispatch")
class PostListView(ListView):
    model = Post

Hands-on Exercise

๐Ÿ‹๏ธ Convert function views into generics

Objective: Rewrite a function-based "list published posts" view and a "create post" view as class-based generics.

Starting point (function-based):

def post_list(request):
    posts = Post.objects.filter(status="published").order_by("-published_date")
    return render(request, "blog/post_list.html", {"posts": posts})

@login_required
def post_create(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            return redirect("blog:post_detail", pk=post.pk)
    else:
        form = PostForm()
    return render(request, "blog/post_form.html", {"form": form})

Requirements:

  1. A PostListView (ListView) that lists published posts, 10 per page, newest first.
  2. A PostCreateView (CreateView) that requires login and stamps the current user as author.
  3. Redirect to the new post's detail page after creation.
๐Ÿ’ก Hint

Set the filtering/ordering in get_queryset(). Add LoginRequiredMixin to the left of CreateView. Set the author inside form_valid via form.instance.author. A Post that defines get_absolute_url() lets CreateView redirect automatically.

โœ… Sample solution
from django.views.generic import ListView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostForm

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

    def get_queryset(self):
        return Post.objects.filter(status="published").order_by("-published_date")

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    template_name = "blog/post_form.html"

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
    # Redirects to post.get_absolute_url() automatically after save.

The two generics replace roughly 15 lines of function code with declarative configuration โ€” and behave identically.

๐ŸŽฏ Quick Quiz

Question 1: What does as_view() do in a URL pattern?

Question 2: Which method should you override to change which objects a ListView shows?

Question 3: In class MyView(LoginRequiredMixin, CreateView), why does the mixin come first?

Best Practices

โœ… Do

  • Reach for a generic view when your page is a standard list/detail/create/update/delete.
  • Put cross-cutting concerns (auth, messaging, ownership checks) in mixins, not duplicated code.
  • Override the smallest hook that does the job โ€” get_queryset, get_context_data, form_valid, get_success_url.
  • Use reverse_lazy for class-level URLs; keep the base view rightmost in the inheritance list.
  • Keep a reference like Classy Class-Based Views handy to see every attribute and method a view provides.

โš ๏ธ Don't

  • Don't force genuinely custom, branchy logic into a generic view โ€” a function view may read more clearly.
  • Don't override dispatch to re-implement auth when a mixin already exists.
  • Don't list mixins after the base view; the base can silently override them.
  • Don't forget that overriding a method usually means calling super() so the built-in behaviour still runs.

Summary & Quiz

๐ŸŽ‰ Key Takeaways

  • as_view() yields a callable; dispatch() routes each request to get(), post(), etc.
  • ListView and DetailView handle read pages; override get_queryset and get_context_data to customise.
  • CreateView / UpdateView / DeleteView handle the whole form lifecycle; use form_valid and get_success_url.
  • Mixins compose behaviour; MRO means mixins go left of the base view.
  • Decorate CBVs with method_decorator; choose CBVs for standard shapes and FBVs for bespoke logic.

๐Ÿ“š Further Reading

๐Ÿš€ What's Next?

Your views now build context dictionaries and hand them to templates. The next lesson dives into the Django Template Language โ€” variables, filters, tags, and template inheritance โ€” so you can turn that context into polished, dynamic HTML.

๐ŸŽ‰ Great progress!

You can now pick the right view style for the job and build full CRUD with a fraction of the code. That's a professional Django habit.