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 viewPurpose
ListViewDisplays a list of objects from a queryset
DetailViewDisplays a single object, looked up by its primary key or slug
CreateViewRenders and processes a form for creating a new object
UpdateViewRenders and processes a form for editing an existing object
DeleteViewRenders 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 viewsClass-based views
Readability for simple casesVery explicit — every line of behavior is visible top-to-bottom in one placeBehavior is spread across inherited methods, which can feel hidden until you're familiar with the base classes
Boilerplate for standard CRUDYou write the same query-render pattern repeatedly by handMuch less repetition — a list or detail page is often four or five attribute lines
Learning curveEasier for beginners — just Python functions and control flowRequires learning what each generic view already does internally before customizing it confidently
Custom, one-off logicStraightforward — write exactly what you needCan 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.

Next up: Every view so far has ended with render() pointing at a template file — Django Template Language is where those .html files actually get explained.