Class-based views and when they help
Django offers a second way to write views: class-based views (CBVs). They package the common patterns — "list these objects", "show this one", "create/update/delete via a form" — into reusable classes you configure rather than write from scratch. Used for what they are good at, they remove a lot of boilerplate; reached for reflexively, they hide behaviour behind inheritance and confuse more than they save. This lesson is what they are, when they genuinely help, and when a plain function view is the better call.
The same view, two ways
Here is the Nidaan patient list as a function view and as a class-based ListView:
# function-based
def patient_list(request):
city = request.GET.get("city", "Pune")
patients = Patient.objects.filter(city=city).order_by("name")
return render(request, "patients/patient_list.html", {"patients": patients, "city": city})
# class-based
from django.views.generic import ListView
class PatientListView(ListView):
model = Patient
template_name = "patients/patient_list.html"
context_object_name = "patients"
def get_queryset(self):
return Patient.objects.filter(city="Pune").order_by("name")
Both render the same page (verified: the ListView returns 200 with the same template and data). The CBV
has no explicit request-handling code — it inherits the "fetch a list, render a template with it" logic
from ListView and you just configure the pieces: which model, which template, what the context variable is
called, and (optionally) which queryset. You wire it to a URL with .as_view():
path("", PatientListView.as_view(), name="patient_list"),
The generic views worth knowing
Django ships a handful of generic CBVs covering the CRUD lifecycle. These are the ones that earn their place:
| View | Does | You provide |
|---|---|---|
ListView |
List objects | model/get_queryset, template_name |
DetailView |
Show one object | model, template_name |
CreateView |
Form to create | model, fields/form_class, success_url |
UpdateView |
Form to edit | model, fields/form_class, success_url |
DeleteView |
Confirm + delete | model, success_url |
CreateView and UpdateView are the biggest win: they build the form, handle GET (show it) and POST
(validate, save, redirect), and re-render with errors — the entire form dance from the function-view lesson,
in a class you configure with a model and a success_url. For standard CRUD on a model, a CBV can replace a
dozen lines of predictable view code.
Customising: the hook methods
You adjust a CBV by overriding hook methods rather than rewriting it:
class PatientListView(ListView):
model = Patient
context_object_name = "patients"
def get_queryset(self): # customise what is listed
city = self.request.GET.get("city", "Pune")
return Patient.objects.filter(city=city).order_by("name")
def get_context_data(self, **kwargs): # add extra context
ctx = super().get_context_data(**kwargs)
ctx["city"] = self.request.GET.get("city", "Pune")
return ctx
get_queryset controls the data; get_context_data adds to the context (always call super() first, then
add). The request is available as self.request. Verified: this ListView with these overrides renders
"Patients in Pune" exactly like the function view. These hooks are the CBV's flexibility — but notice you
have to know which hook to override, and that is precisely the learning curve people complain about.
The honest trade-off: when CBVs help and when they hurt
CBVs are not universally better or worse — they trade explicitness for reuse, and the right choice depends on the view:
Use a class-based view when:
- The view is standard CRUD on a model —
ListView,DetailView,CreateView,UpdateView,DeleteViewdo exactly what you need with minimal configuration. This is their sweet spot. - You have many similar views and the shared behaviour is worth factoring into a base class.
Use a function-based view when:
- The logic is custom or unusual — multiple models, conditional flows, anything that does not map to a generic view. Forcing it into a CBV means overriding so many hooks that you have written more code than a function, in a harder-to-follow shape.
- Readability matters more than reuse — a function view's logic is right there, top to bottom, with no need to know the inheritance chain. For a one-off or a junior-friendly codebase, that clarity is worth a few extra lines.
The real cost of CBVs is that behaviour is inherited and implicit: to know what CreateView does you
must know its parent classes and their method resolution order, which is genuinely hard to trace (the
community built the "Classy Class-Based Views" site precisely because the inheritance is so hard to read).
A function view hides nothing. So the honest rule: reach for a generic CBV when your view is the
standard pattern it implements; write a function view when it is not. Do not convert a clear function view
into a maze of overridden hooks for the sake of using classes — that is the common over-correction, and it
makes code worse, not better.
Check your work
What a CBV is. A class that packages a common view pattern; you configure attributes/hooks instead of
writing request handling. Verified: ListView renders the same page as the function view with no explicit
handler code.
The generic views. ListView, DetailView, CreateView, UpdateView, DeleteView — CRUD; Create/
Update handle the whole form GET/POST/validate/redirect dance from configuration.
How to wire and customise. .as_view() in the URL; override get_queryset (data) and
get_context_data (extra context, call super() first); the request is self.request.
When CBVs help. Standard CRUD on a model, and families of similar views worth a shared base — their sweet spot.
When function views are better. Custom/unusual logic and readability-first code — where forcing a CBV means overriding many hooks and hiding behaviour in inheritance.
The core trade-off. CBVs trade explicitness for reuse; their cost is implicit, inherited behaviour that is hard to trace — use a generic CBV when your view is that pattern, a function view when it is not.
Practice
- Rewrite
patient_listas aListViewwithget_querysetandget_context_data; confirm it renders identically (200, same template). - Build a
DetailViewfor a patient and wire it with<int:pk>; confirm it shows one patient. - Build a
CreateViewforPatientwithfieldsand asuccess_url; confirm it handles GET and POST with no explicit method code. - Take a view with custom multi-model logic and try to express it as a CBV; note how many hooks you must override, then compare with the function version. Decide which is clearer.
- Look up one generic view on the "Classy Class-Based Views" reference and trace one method through its parents — feel the implicit inheritance the lesson warns about.
- For three Nidaan views, decide CBV or function view and justify each against the trade-off.
Official documentation
- Django — Class-based views — Overview and the generic views.
- Django — Built-in class-based generic views — Every generic view and its attributes/hooks.
- Classy Class-Based Views — A community reference that flattens the inheritance so you can see every method.
Next: forms and ModelForms.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship