Class-Based Views
Every function-based view in the previous lesson followed a familiar shape: fetch some data,
hand it to render(), done. Class-based views (CBVs) package that
recurring shape into a Python class you configure with a few attributes instead of writing out by
hand. They're not a replacement for function-based views — they're a second tool that fits certain
jobs, particularly repetitive CRUD-style pages, noticeably better.
Class-Based Views: A Different Shape for the Same Job
A CBV is a Python class where individual methods, like get() and post(),
handle individual HTTP methods — instead of one function branching on request.method
internally the way toggle_task_complete did in the previous lesson. Django's
generic views take this further: for very common patterns like "show a list of
objects" or "show one object," Django ships ready-made base classes that already know how to do the
fetching and rendering, and just need to be told which model and template to use.
Django's Built-In Generic Views
| Generic view | Purpose |
|---|---|
ListView | Displays a list of objects from a queryset |
DetailView | Displays a single object, looked up by its primary key or slug |
CreateView | Renders and processes a form for creating a new object |
UpdateView | Renders and processes a form for editing an existing object |
DeleteView | Renders a confirmation page and deletes an object on confirm |
CreateView, UpdateView, and DeleteView lean heavily on
Django Forms and ModelForms, which get their own dedicated lessons later in this track — this
lesson focuses on ListView and DetailView, which need nothing more than a
model.
From Function to Class: Rewriting task_list
Recall the function-based task_list view: query all tasks, order them, render a
template with that queryset in context. As a ListView, that becomes:
# taskflow/views.py
from django.views.generic import ListView
class TaskListView(ListView):
model = Task
template_name = 'taskflow/task_list.html'
context_object_name = 'tasks'
Four lines replace what was previously a function body. model = Task tells
ListView what to query (it defaults to Task.objects.all());
template_name tells it what to render; context_object_name renames the
default context key (which would otherwise be object_list) to the friendlier
tasks, matching what the template already expects. Notice there's no explicit
get() method, no call to render() — ListView already knows
how to do both.
A Single-Object View: TaskDetailView
DetailView follows the same idea for one object, and it already knows how to pull the
primary key out of the URL and 404 automatically if nothing matches — no manual
get_object_or_404() call required.
# taskflow/views.py
from django.views.generic import DetailView
class TaskDetailView(DetailView):
model = Task
template_name = 'taskflow/task_detail.html'
context_object_name = 'task'
Paired with a URL pattern using <int:pk> — the parameter name DetailView
looks for by default — the class handles the lookup, the 404 case, and the render, all from those
four lines.
Wiring a CBV into urls.py with .as_view()
path() expects a callable it can invoke with a request — but TaskListView
is a class, not a function. .as_view() is a classmethod every CBV inherits that
returns a plain function wrapping the class: that returned function is what actually gets called
per request, and internally it creates an instance of the class and dispatches to the right method.
# taskflow/urls.py
from django.urls import path
from .views import TaskListView, TaskDetailView
urlpatterns = [
path('tasks/', TaskListView.as_view(), name='task-list'),
path('tasks/<int:pk>/', TaskDetailView.as_view(), name='task-detail'),
]
Forgetting the parentheses — writing TaskListView instead of
TaskListView.as_view() — is a very common mistake, and it fails immediately, since
path() is handed the class itself rather than a callable view function.
Customizing Behavior: Overriding get_queryset()
The four-line TaskListView above shows every task. To show only incomplete ones,
override get_queryset() — the method ListView calls internally to decide
what to display — rather than hardcoding a queryset as a class attribute.
# taskflow/views.py
class IncompleteTaskListView(ListView):
model = Task
template_name = 'taskflow/task_list.html'
context_object_name = 'tasks'
def get_queryset(self):
return Task.objects.filter(completed=False).order_by('due_date')
This is the general pattern for customizing any generic view: rather than fighting the defaults, override the specific method responsible for the piece of behavior you want to change, and let the rest of the class keep doing its job.
FBVs vs CBVs: An Honest Comparison
Neither style is universally "better" — they suit different situations, and most real Django projects use both.
| Function-based views | Class-based views | |
|---|---|---|
| Readability for simple cases | Very explicit — every line of behavior is visible top-to-bottom in one place | Behavior is spread across inherited methods, which can feel hidden until you're familiar with the base classes |
| Boilerplate for standard CRUD | You write the same query-render pattern repeatedly by hand | Much less repetition — a list or detail page is often four or five attribute lines |
| Learning curve | Easier for beginners — just Python functions and control flow | Requires learning what each generic view already does internally before customizing it confidently |
| Custom, one-off logic | Straightforward — write exactly what you need | Can require fighting the class's assumptions, or falling back to a plain View subclass |
A reasonable default: reach for a generic CBV when a view really is "list of X" or "detail of X" with little else going on, and reach for a function-based view the moment the logic branches in ways that don't map cleanly onto one of Django's generic classes.
render() pointing at a
template file — Django Template Language is where those
.html files actually get explained.