Understand Django class-based views (CBVs). Covers ListView, DetailView, CreateView, UpdateView, mixins, and when to use CBVs vs FBVs.
Django offers two ways to write views: function-based views, which are explicit and beginner-friendly, and class-based views, which trade a little initial complexity for a great deal of reusability and built-in functionality. Class-based views can feel mysterious at first because so much happens through inheritance, but once you understand the model, they let you build common patterns — list pages, detail pages, forms — with remarkably little code. This guide makes class-based views clear and practical.
Every Django view turns a request into a response, and you can write that as a function or as a class. Function-based views are a single function that receives the request and returns a response — explicit, easy to read, and a great place to start. Class-based views express the same thing as a class, using inheritance to provide common behavior so you write less. Neither is universally better; they are tools for different situations. Understanding that both styles do the same fundamental job, just organized differently, sets the right frame for learning when each shines.
The motivation for class-based views is that web applications repeat the same patterns constantly — showing a list of objects, showing one object's detail, handling a create or edit form. Writing these from scratch as functions every time means repeating boilerplate. Class-based views capture these common patterns as reusable classes you customize, so a standard list page becomes a few lines rather than many. The payoff is less repetitive code and consistency across your views. Recognizing that class-based views exist to eliminate the boilerplate of common patterns explains why, despite their initial learning curve, they are so widely used for standard pages.
A class-based view works through inheritance: you subclass one of Django's view classes and set a few attributes or override a few methods to customize it, and the parent class provides the rest of the behavior. When a request arrives, the class routes it to a method based on the HTTP method — a GET request is handled differently from a POST — and the inherited machinery handles the common flow. This is the source of both their power and their initial mystery: much of what happens is provided by the parent class rather than written by you. Understanding that you are customizing inherited behavior is the key that unlocks them.
You never instantiate a class-based view yourself. In the URLconf you call as_view(), which returns an ordinary function. Each time a request matches that URL, the function creates a fresh instance of your class, calls setup() to store self.request, self.args and self.kwargs, and then calls dispatch(), which looks up a method named after the lowercased HTTP verb.
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.ArticleListView.as_view(), name="article-list"),
path("new/", views.ArticleCreateView.as_view(), name="article-create"),
path("<slug:slug>/", views.ArticleDetailView.as_view(), name="article-detail"),
path("<slug:slug>/edit/", views.ArticleUpdateView.as_view(), name="article-update"),
path("<slug:slug>/delete/", views.ArticleDeleteView.as_view(), name="article-delete"),
]
A few consequences follow from this design. Because a new instance is created per request, setting self.something inside a handler is safe — it cannot leak into another user's request. Class-level mutable attributes are a different story: a list or dict declared on the class body is shared by every instance, so appending to it at request time is a real bug. A request using a verb with no matching method receives a 405 Method Not Allowed automatically, and HEAD falls back to your get() if you have not defined head(). Finally, any keyword arguments passed to as_view() — for example as_view(template_name="about.html") — must correspond to attributes that already exist on the class; Django raises a TypeError for unknown names, which catches typos early.
The simplest class-based view renders a template, and it is a gentle entry point. You specify which template to use and optionally provide extra data for it, and the view handles displaying it. This suits static or mostly-static pages — an about page, a landing page — where you just need to render a template, perhaps with a little context. Starting with this simple case shows the class-based pattern clearly without much complexity: you set an attribute for the template, optionally override a method to add data, and the parent class does the work of producing the response.
One of the most useful class-based views displays a list of objects — a list of products, articles, or users. You tell it which model to list, and it fetches the objects, makes them available to a template, and even provides pagination, all from a few lines. This captures an extremely common pattern with minimal code, and it is where the value of class-based views becomes obvious. Customizing which objects appear — filtering the list, ordering it — is done by overriding a method, giving you full control while still benefiting from all the built-in behavior the list view provides for free.
class ArticleListView(ListView):
model = Article
paginate_by = 20
context_object_name = "articles"
template_name = "blog/article_list.html"
def get_queryset(self):
qs = (
Article.objects.filter(published=True)
.select_related("author", "category")
.order_by("-created")
)
q = self.request.GET.get("q", "").strip()
if q:
qs = qs.filter(title__icontains=q)
return qs
Pagination reads the ?page= parameter, so when you build page links in the template, carry the search term into them as well (?page=2&q=django); forgetting it is the classic bug where page 2 of a filtered list suddenly shows unfiltered results. The context a ListView provides is summarised below.
| Context variable | Contents |
|---|---|
object_list | The objects for the current page (or all objects if not paginated) |
article_list / your context_object_name | The same list under a friendlier name |
page_obj | The current Page, or None without pagination |
paginator | The Paginator instance, or None |
is_paginated | True only when there is more than one page |
view | The view instance itself, so templates can call its methods |
The ordering attribute is applied by the default get_queryset(), so if you override get_queryset() without calling super(), as above, the attribute is ignored; pick one place to order and stay consistent. Always order a paginated queryset — Django emits an UnorderedObjectListWarning otherwise, because an unordered query can return overlapping or missing rows between pages.
The companion to the list is the detail view, which displays a single object — one product page, one article. You specify the model, and it fetches the right object based on the URL and makes it available to the template. Like the list view, it captures a ubiquitous pattern with very little code. Together, list and detail views handle the read side of most applications — browsing a collection and viewing individual items — and the small amount of code they require, compared to writing the same logic by hand, demonstrates clearly why class-based views are favored for these standard read pages.
Class-based views also streamline the create-update-delete operations that involve forms. The create and update views handle displaying a form, validating the submitted data, saving the object, and redirecting on success — the full form-handling flow that is tedious to write by hand each time. You specify the model and fields, and the view manages the rest, including re-displaying the form with errors when validation fails. This is some of the most valuable functionality class-based views provide, because form handling has a lot of repetitive boilerplate, and capturing it in reusable views removes a substantial amount of error-prone code.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, UpdateView
from .forms import ArticleForm
class ArticleCreateView(LoginRequiredMixin, CreateView):
model = Article
form_class = ArticleForm # template: blog/article_form.html
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
model = Article
form_class = ArticleForm # same template; object is in context
def get_queryset(self):
return Article.objects.filter(author=self.request.user)
class ArticleDeleteView(LoginRequiredMixin, DeleteView):
model = Article # template: blog/article_confirm_delete.html
success_url = reverse_lazy("blog:article-list")
def get_queryset(self):
return Article.objects.filter(author=self.request.user)
Here ArticleForm is an ordinary ModelForm listing title, slug, category, body and published. Create and update views share the _form template suffix, so a single template serves both; check {% if object %} in it to change the heading. Neither create nor update view needs a success_url here because Article defines get_absolute_url(), which the views use by default. The delete view has no such fallback, and it must use reverse_lazy() because the URLconf is not loaded yet when the class body executes at import time.
List form fields explicitly, either in a ModelForm or with the fields attribute. Django refuses to build a model form view with neither (it raises ImproperlyConfigured), and using fields = "__all__" is risky: a field added to the model later — say, is_featured or author — silently becomes user-editable. Keeping the author field out of the form and setting it in form_valid() is what stops a user from submitting someone else's ID. Since Django 4.0, DeleteView processes the POST through a form as well, so custom logic that runs on deletion belongs in form_valid() rather than in an overridden delete() method, which older tutorials still show.
A powerful aspect of class-based views is mixins — small classes that add a specific piece of behavior, which you combine with a view to compose functionality. A common example adds a requirement that the user be logged in, applied by including the appropriate mixin. Mixins let you build views by combining reusable behaviors rather than rewriting them, which is the deeper reason class-based views are so flexible. Understanding mixins as composable units of behavior you mix into a view explains how class-based views achieve their reusability, and it is the concept that lets you tailor views precisely while still reusing common functionality.
from django.contrib.auth.mixins import (
LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin,
)
class ModerationQueueView(PermissionRequiredMixin, ListView):
permission_required = "blog.change_article"
raise_exception = True # 403 instead of redirect to login
queryset = Article.objects.filter(published=False).order_by("created")
template_name = "blog/moderation_queue.html"
class ArticleOwnerRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.get_object().author_id == self.request.user.pk
class ArticlePublishView(LoginRequiredMixin, ArticleOwnerRequiredMixin, UpdateView):
model = Article
fields = ["published"]
Order matters. Python resolves methods left to right through the class's MRO (method resolution order), and the access mixins do their work in dispatch(), so they must appear to the left of the generic view in the base list — otherwise the view's own dispatch() runs first and the check never happens. LoginRequiredMixin should come first so anonymous users are redirected to the login page before any permission test runs. You can inspect the resolved order with ArticlePublishView.__mro__ in a shell, which is the quickest way to answer "which get_context_data() is actually running?"
The way you tailor a class-based view's behavior is by overriding specific methods that the parent class calls at known points — to change which objects a list returns, to add extra data to a template's context, to adjust what happens after a form is saved. Each override hooks into a precise step of the inherited flow. Knowing which method to override for which customization is the practical skill of working with class-based views, and Django's documentation maps these out. Once you know the key methods and what each controls, customizing a class-based view becomes straightforward — you reach into exactly the step you need to change.
The honest guidance is to use each style where it fits. Class-based views excel at the standard patterns — list, detail, and form pages — where they save substantial code and provide consistency. Function-based views are often clearer for views with custom, one-off logic that does not match a standard pattern, where the explicitness of a function is easier to follow than navigating inherited methods. Many projects use both, choosing per view. Rather than treating one style as superior, recognizing that class-based views shine for common patterns and function-based views for bespoke logic lets you pick the right tool for each situation.
The honest barrier with class-based views is that their behavior is spread across parent classes you do not see, which makes them feel opaque compared to a function you can read top to bottom. The way past this is to learn the small number of methods and attributes that each common view exposes for customization, and to consult Django's documentation or tools that show the full method flow. Once you have seen the pattern a few times — set the model, set the template, override the one method you need — the opacity lifts and the productivity becomes obvious. The learning curve is real but short, and it pays off across every standard page you build afterward.
A very common customization is providing extra data to a view's template beyond what it supplies by default — perhaps a list of categories alongside the main object, or some computed value. Class-based views offer a specific method you override to add to the data passed to the template, where you take the existing context, add your own entries, and return it. This is one of the first overrides most people learn because the need is so frequent. Understanding this single hook — how to inject additional context — unlocks a large fraction of the customization that real pages require, while keeping all the view's other built-in behavior intact.
For list and detail views, you frequently need to change which objects are shown — filtering a list to the current user's items, excluding inactive records, or ordering results. Class-based views provide a method you override to control exactly which objects the view works with, returning a customized query instead of the default of all objects. This is how you tailor what a list displays or restrict which object a detail view can show, which is essential for both functionality and security. Mastering this override, alongside adding context, covers most of what you need to make these views do precisely what your application requires.
Consider an update view that uses model = Article with no custom queryset. Any logged-in user who changes the slug in the URL can edit any article — an insecure direct object reference, one of the most common authorization flaws in web applications. The fix is not an if statement in the template; it is a queryset that only contains objects the current user may act on:
class OwnedByUserMixin:
"""Limit single-object and list views to the requesting user's rows."""
owner_field = "author"
def get_queryset(self):
return super().get_queryset().filter(**{self.owner_field: self.request.user})
class ArticleUpdateView(LoginRequiredMixin, OwnedByUserMixin, UpdateView):
model = Article
form_class = ArticleForm
Because get_object() looks the slug up inside this filtered queryset, another user's article produces a 404, not a 403 — which also avoids confirming that the record exists. Putting this in a mixin means every view that includes it gets the same rule, and a code reviewer can confirm authorization by reading the class declaration.
With the form-handling views, a common need is controlling what happens after a successful submission — where to redirect, or performing some action before saving. Class-based views provide hooks for this: specifying the destination after success, and overriding the point where the form is valid to add custom logic such as setting a field based on the current user before the object is saved. These hooks let you insert your application's specific behavior into the standard form flow without rewriting it. Knowing where to intervene in the form lifecycle is what lets the form views handle the boilerplate while you supply just the custom pieces your feature needs.
In practice, a typical Django project uses class-based views for its standard pages — listing and viewing records, handling create and edit forms — and function-based views for anything with unusual, one-off logic. This mix is not a compromise but the sensible outcome of using each style where it fits. The list and detail pages, the forms, the simple template pages all become concise class-based views, while a complex custom workflow stays an explicit function. Seeing how the two coexist in real projects, chosen view by view based on whether the page matches a standard pattern, is the practical wisdom that makes class-based views a productive tool rather than a source of confusion.
Class-based views are tested the same way as function views — through the test client, asserting on what a user can observe. Focus the tests on the behaviour you configured or overrode, not on the generic machinery Django already tests.
# blog/tests/test_views.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from blog.models import Article, Category
User = get_user_model()
class ArticleViewTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.alice = User.objects.create_user("alice", password="pw-alice-123")
cls.bob = User.objects.create_user("bob", password="pw-bob-123")
cat = Category.objects.create(name="News")
cls.article = Article.objects.create(
title="Hello", slug="hello", body="...", author=cls.alice,
category=cat, published=True,
)
def test_list_is_paginated(self):
resp = self.client.get(reverse("blog:article-list"))
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "blog/article_list.html")
self.assertIn("page_obj", resp.context)
def test_anonymous_create_redirects_to_login(self):
resp = self.client.get(reverse("blog:article-create"))
self.assertEqual(resp.status_code, 302)
self.assertIn("/login/", resp.url)
def test_other_user_cannot_edit(self):
self.client.force_login(self.bob)
url = reverse("blog:article-update", kwargs={"slug": "hello"})
self.assertEqual(self.client.get(url).status_code, 404)
self.assertEqual(self.client.post(url, {"title": "Hacked"}).status_code, 404)
For logic buried in a single hook, you can also instantiate the view directly, call setup(request, **kwargs) with a request from RequestFactory, and call the method under test. The ownership test posts as well as gets, because a view that blocks the edit form but still accepts the submitted POST is a real and easily missed bug. The login redirect assertion assumes the default LOGIN_URL; adjust it if your project changes that setting.
Most class-based view problems produce one of a handful of messages. The cause is almost always a missing attribute, a missing super() call, or a mixin in the wrong position.
| Symptom | Likely cause | Fix |
|---|---|---|
TemplateDoesNotExist: blog/article_list.html | The inferred template name does not match your file | Create the file at that path or set template_name |
ImproperlyConfigured: Using ModelFormMixin (base class of ...) without the 'fields' attribute is prohibited | A create or update view with neither fields nor form_class | Add a ModelForm or an explicit field list |
ImproperlyConfigured: No URL to redirect to | No success_url and the model lacks get_absolute_url() | Define one or the other |
ImproperlyConfigured: ... is missing a QuerySet | No model, queryset or get_queryset() | Set model or return a queryset |
| Template variable suddenly missing | get_context_data() without super() | Start from super().get_context_data(**kwargs) |
| Login check ignored | Mixin listed after the generic view | Put access mixins leftmost |
| 405 on form submit | Posting to a view that only defines get(), such as DetailView | Post to a form view, or add a post() handler |
When the cause is not obvious, reading the source is faster than guessing. Every generic view is a short chain of mixins in django/views/generic/; open edit.py or list.py from your virtual environment and follow the methods your class inherits. A breakpoint in your own override, combined with type(self).__mro__, tells you which parent runs next.
Class-based views let you build Django's common page patterns — lists, details, and forms — with far less code than writing them by hand, by capturing those patterns as reusable classes you customize through inheritance. They can feel mysterious at first because much of their behavior comes from parent classes, but the model is simple: subclass a view, set a few attributes, and override specific methods to tailor the inherited flow. TemplateView renders simple pages, ListView and DetailView handle the read side of most apps, and the form-handling views manage the tedious create-update-delete flow. Mixins let you compose reusable behaviors like login requirements, and method overrides hook into precise steps for customization. Use class-based views for standard patterns where they save real effort, and function-based views for bespoke logic where explicitness is clearer — and with that judgment, both styles become complementary tools for writing clean, efficient Django views.