๐๏ธ 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()anddispatch()route a request toget(),post(), etc. - Build read pages with ListView and DetailView, customising
get_querysetandget_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"),
]
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.
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:
- A
PostListView(ListView) that lists published posts, 10 per page, newest first. - A
PostCreateView(CreateView) that requires login and stamps the current user as author. - 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_lazyfor 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
dispatchto 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 toget(),post(), etc.- ListView and DetailView handle read pages; override
get_querysetandget_context_datato customise. - CreateView / UpdateView / DeleteView handle the whole form lifecycle; use
form_validandget_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
- Django Docs โ Class-based views
- Django Docs โ Using mixins with CBVs
- Classy Class-Based Views โ attribute/method reference
๐ 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.