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 of reverse_lazy() in success_url.
  • Forgetting to call super() in get_context_data, losing the default context.
  • Overriding get() or post() entirely when a small hook such as get_queryset would 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.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India