Class-based views — the same job, structured differently
A CBV isn't a different way to handle requests — it's the same view function, restructured as a class so common patterns (list this, show one, save a form) don't have to be hand-written every time.
5 min read
What a class-based view actually is
from django.views import View
from django.http import HttpResponse
class HelloView(View):
def get(self, request):
return HttpResponse("Hello, GET!")
def post(self, request):
return HttpResponse("Hello, POST!")# urls.py
urlpatterns = [
path("hello/", HelloView.as_view()),
]A function-based view is one function that has to check request.method itself to branch between GET and POST, the way the forms lesson covers. A class-based view (CBV) instead defines one method per HTTP verb — get(), post(), put(), delete() — and Django's View base class handles the routing between them internally. HelloView.as_view() is the actual thing registered in urls.py; it's a function that, when called, constructs an instance of HelloView and dispatches the request to whichever method matches request.method. The behavior is identical to a function-based view with an if request.method == "POST": check — this is a different structure for the same job, not a different capability.
Why this exists: repeating the same shape constantly
# Function-based: this exact shape gets rewritten for every model
def article_list(request):
articles = Article.objects.all()
return render(request, "articles/list.html", {"articles": articles})
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(request, "articles/detail.html", {"article": article})"Fetch a queryset, render a list template" and "fetch one object by primary key, render a detail template" are two shapes that show up on nearly every model in nearly every Django app. Writing them by hand every time is real, repetitive boilerplate — the same handful of lines, model after model, with only the model name and template path actually changing.
Generic class-based views: the same shapes, provided for you
from django.views.generic import ListView, DetailView
class ArticleListView(ListView):
model = Article
template_name = "articles/list.html"
context_object_name = "articles"
class ArticleDetailView(DetailView):
model = Article
template_name = "articles/detail.html"
context_object_name = "article"# urls.py
urlpatterns = [
path("articles/", ArticleListView.as_view()),
path("articles/<int:pk>/", ArticleDetailView.as_view()),
]ListView already knows how to fetch Article.objects.all(), put it in the template context as articles, and render articles/list.html — the exact function-based view from above, expressed as three lines of configuration instead of a hand-written function body. DetailView does the equivalent for a single object looked up by URL parameter. Neither of these classes contains application-specific logic; they're pre-built implementations of the two most common view shapes, customized entirely through class attributes.
The other generic views, briefly
CreateView/UpdateView/DeleteView— the form-handling equivalents: render a form on GET, validate and save on POST (CreateView/UpdateView), or render a confirmation and delete on POST (DeleteView). Each needsmodeland eitherfieldsor aform_class, plus usuallysuccess_urlfor where to redirect after saving.TemplateView— for a page that just renders a template with no model involved at all (an "About" page, a static landing page).RedirectView— for a URL whose entire job is redirecting somewhere else.
The pattern across all of them is the same: a generic class already implements the common shape, and a subclass fills in a handful of class attributes (model, template_name, fields) instead of writing the whole view body from scratch.
Overriding one piece without rewriting the whole view
class PublishedArticleListView(ListView):
model = Article
template_name = "articles/list.html"
context_object_name = "articles"
def get_queryset(self):
return Article.objects.filter(published=True).order_by("-published_at")This is the real reason CBVs are structured as classes and not just configuration dictionaries: ListView breaks its work into overridable methods (get_queryset(), get_context_data(), and others), so customizing one specific piece of the behavior — here, filtering to only published articles — means overriding just that one method, while everything else ListView already does (pagination, template rendering, context setup) keeps working unchanged. A function-based view doesn't have this kind of built-in seam; customizing part of its behavior means editing the function directly, in whatever shape that function happens to already be in.
The real trade-off: less code, more indirection
# Function-based: every line of behavior is visible, top to bottom, in one place
def article_list(request):
articles = Article.objects.all()
return render(request, "articles/list.html", {"articles": articles})
# Class-based: behavior is inherited, not visible directly in this file
class ArticleListView(ListView):
model = Article
template_name = "articles/list.html"The generic CBV version genuinely has less code to write, and the common cases (list, detail, create, update, delete) are implemented once by Django and battle-tested. The cost: understanding what ArticleListView actually does on a request requires knowing what ListView does internally — get_queryset(), get_context_data(), and the exact order they're called in aren't visible in this file at all, unlike a function-based view where every line of behavior is right there to read. This is a genuine readability trade-off, not a strictly-better upgrade — Django's own documentation is explicit that function-based views remain a completely valid choice, especially for views with enough custom logic that fighting a generic class's structure would cost more than it saves.
When to reach for which
A view that's a close match for one of the generic shapes (list a model, show one, handle its create/update/delete form) is usually less code and less to get wrong as a CBV. A view with logic that doesn't map cleanly onto those shapes — combining several unrelated things, or branching in ways that don't correspond to "GET vs. POST" — is often clearer as a plain function, where every step is visible without needing to know an inheritance chain.
Further reading
- Django docs — class-based views
- Django docs — built-in generic display views
- Classy Class-Based Views — a reference showing every method and attribute each generic CBV actually inherits.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does HelloView.as_view() actually return, and how is that different from HelloView itself?
2. What is ListView(model=Article, template_name='articles/list.html') equivalent to, in function-based terms?
3. Why does overriding get_queryset() on a ListView subclass only affect the filtering, not the pagination or template rendering?
4. What real cost does a generic CBV introduce compared to a function-based view, according to this lesson?