Class-based & generic views
Function views are simple and explicit. But most pages follow the same patterns — list objects, show one, create, edit, delete — and Django's generic class-based views (CBVs) implement those patterns for you. You override only what differs.
From function to class
# shop/views.py
from django.views.generic import ListView, DetailView
from .models import Product
class ProductListView(ListView):
model = Product
paginate_by = 20
ordering = ["name"]
# template: shop/product_list.html, context: object_list / product_list / page_obj
class ProductDetailView(DetailView):
model = Product
# template: shop/product_detail.html, context: object / product
# shop/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("products/", views.ProductListView.as_view(), name="product_list"),
path("products/<int:pk>/", views.ProductDetailView.as_view(), name="product_detail"),
]
as_view() turns the class into a callable the URL resolver can use. Each request gets a fresh instance.
The generic views you'll use most
| View | Purpose | Default template |
|---|---|---|
TemplateView |
Static page with context | set template_name |
ListView |
Many objects, optional pagination | <app>/<model>_list.html |
DetailView |
One object by pk or slug |
<app>/<model>_detail.html |
CreateView |
Form to create an object | <app>/<model>_form.html |
UpdateView |
Form to edit an object | <app>/<model>_form.html |
DeleteView |
Confirm and delete | <app>/<model>_confirm_delete.html |
Customising with hook methods
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView
class ProductListView(ListView):
model = Product
paginate_by = 20
def get_queryset(self):
qs = super().get_queryset().filter(in_stock=True)
q = self.request.GET.get("q")
return qs.filter(name__icontains=q) if q else qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["search"] = self.request.GET.get("q", "")
return ctx
class ProductCreateView(LoginRequiredMixin, CreateView):
model = Product
fields = ["name", "price", "in_stock"]
success_url = reverse_lazy("product_list")
def form_valid(self, form):
form.instance.created_by = self.request.user
return super().form_valid(form)
Use reverse_lazy for class attributes, because URLs are not loaded yet when the module is imported.
Mixins and method resolution order
Mixins add behaviour: LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin. Python resolves
methods left to right, so access-control mixins must come before the generic view in the class definition.
When to choose which
- Function views: unusual flows, several forms on one page, beginners reading your code.
- Generic CBVs: standard CRUD pages where the defaults fit 80% of the work.
Neither is "better"; many projects mix both.
Common mistakes
- Writing
class MyView(CreateView, LoginRequiredMixin)— the mixin is placed too late and does not protect the view. - Using
reverse()instead ofreverse_lazy()insuccess_url. - Forgetting to call
super()inget_context_data, losing the default context. - Overriding
get()orpost()entirely when a small hook such asget_querysetwould do.
Practice
Convert a product list and edit page from function views to ListView and UpdateView. Add a search box using
get_queryset, restrict editing to logged-in staff with a mixin, and paginate at 12 items per page.