Django

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.

Intermediate

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 needs model and either fields or a form_class, plus usually success_url for 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

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?