Filter concepts by levelShowing all levels.

Django · Class-Based Views

Class-Based Views

How dispatch() routes by method, the generic view family, and composing mixins safely

  • as_view() returns a functiondispatch() routes to get()/post()/etc.
  • get_queryset()/get_object()the seams every generic view exposes
  • form_valid()/form_invalid()the fork every FormView-based view hits
  • Mixin order = MRO orderlist the mixin before the base view

1.The View base class and dispatch()

Every class-based view subclasses View, whose as_view() returns a plain function Django can call like any FBV. dispatch() is what that function actually runs — it looks at request.method and calls the matching method (get(), post(), ...) on the instance.

Key features

  • MyView.as_view() returns a function — that function, not the class, is what urls.py actually calls
  • dispatch(self, request, *args, **kwargs) looks up self.get/self.post/etc. based on request.method.lower()
  • A method not defined on the view (e.g. no post() on a GET-only view) falls through to http_method_not_allowed — a 405
  • get(), post(), put(), delete() etc. all receive the same (self, request, *args, **kwargs) signature
  • Every generic view (TemplateView, ListView, DetailView...) is ultimately a View subclass — dispatch() is inherited, not reimplemented

What as_view() and dispatch() actually do

What as_view() and dispatch() actually do
StepWhat happens
MyView.as_view()returns a plain function, called `view`, closing over the class
view(request, ...)creates self = MyView(), then calls self.dispatch(request, ...)
dispatch()picks self.get/self.post/... by request.method.lower()
no matching methodfalls through to http_method_not_allowed() — a 405
How a request reaches get()/post()

urlpatterns

MyView.as_view()

dispatch()

checks request.method

self.get() / self.post()

  • urlpatterns — MyView.as_view()
    • leads to dispatch()
  • dispatch() — checks request.method
    • leads to self.get() / self.post()
  • self.get() / self.post()

Command

python
from django.views import View
from django.http import HttpResponse

class Echo(View):
    def get(self, request):
        return HttpResponse("GET received")

    def post(self, request):
        return HttpResponse("POST received")

MyView.as_view() returns a function urls.py can call; dispatch() (inherited, not written by hand) routes to get()/post()/etc. based on request.method — a missing method becomes an automatic 405.

2.TemplateView and ListView

TemplateView renders a template with no database work by default — override get_context_data() to add data. ListView renders a queryset of objects — override get_queryset() to filter it, and paginate_by to page it automatically.

Key features

  • TemplateView needs only template_name to render something — no model required
  • ListView needs model or queryset — context_object_name defaults to "<model>_list" if not set
  • get_queryset() is where filtering, ordering, or per-user scoping belongs — never override queryset as a class attribute for anything dynamic
  • paginate_by turns on automatic pagination — the template then receives page_obj and is_paginated too
  • get_context_data(**kwargs) must call super().get_context_data(**kwargs) first, or the parent's own context (like the object list) is lost

TemplateView vs ListView

TemplateView vs ListView
AspectTemplateViewListView
Requirestemplate_namemodel or queryset
Default context keynone"<model>_list" (or context_object_name)
Customize the dataget_context_data()get_queryset()
Paginationnot built inpaginate_by, automatic
TemplateView vs. ListView

TemplateView

  • Needs only template_name
  • No database work by default
  • Customize via get_context_data()

ListView

  • Needs model or queryset
  • context_object_name defaults to "<model>_list"
  • Customize via get_queryset(); paginate_by for paging
  • TemplateView
    • Needs only template_name
    • No database work by default
    • Customize via get_context_data()
  • ListView
    • Needs model or queryset
    • context_object_name defaults to "<model>_list"
    • Customize via get_queryset(); paginate_by for paging

Command

python
class DashboardView(TemplateView):
    template_name = "dashboard.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["total_users"] = User.objects.count()
        return context

TemplateView needs only template_name; ListView adds a queryset — override get_queryset() to filter it, and always call super().get_context_data() first when adding extra context.

3.DetailView, CreateView, UpdateView, DeleteView

DetailView fetches a single object via get_object() (by pk_url_kwarg, default "pk"). CreateView/UpdateView both wrap a ModelForm and redirect to success_url on save; DeleteView shows a confirmation on GET and deletes on POST.

Key features

  • DetailView's get_object() looks up by pk_url_kwarg (default "pk") or slug_url_kwarg — override for any other lookup
  • CreateView/UpdateView both need fields (or a form_class) and success_url (or a get_absolute_url() on the model)
  • UpdateView calls get_object() on GET to pre-fill the form — CreateView never does, since there is no existing object yet
  • DeleteView renders a confirmation template on GET and only deletes on POST — never on GET, since GET must stay a safe method
  • All three editing views inherit form_valid()/form_invalid() from FormMixin — see the form-handling concept for what those do

DetailView and the three editing views

DetailView and the three editing views
ViewGET doesPOST does
DetailViewrenders the object— (read-only)
CreateViewrenders a blank formvalidates and creates
UpdateViewrenders form pre-filled from get_object()validates and saves
DeleteViewrenders a confirmation pagedeletes and redirects
What GET and POST do on each editing view
DetailView (GET)
renders the object
CreateView (GET)
renders a blank form
UpdateView (GET)
renders pre-filled form
DeleteView (GET)
renders confirmation only
CreateView/UpdateView (POST)
validates and saves
DeleteView (POST)
deletes and redirects
  • DetailView (GET): read-only, renders a form/page — renders the object
  • CreateView (GET): between read-only and destructive, renders a form/page — renders a blank form
  • UpdateView (GET): between read-only and destructive, renders a form/page — renders pre-filled form
  • DeleteView (GET): destructive, renders a form/page — renders confirmation only
  • CreateView/UpdateView (POST): between read-only and destructive, commits a change — validates and saves
  • DeleteView (POST): destructive, commits a change — deletes and redirects

Command

python
class ArticleUpdateView(UpdateView):
    model = Article
    fields = ["title", "body"]
    success_url = reverse_lazy("articles:list")

DetailView/CreateView/UpdateView share get_object()/ModelForm plumbing; DeleteView deliberately splits GET (confirm) from POST (actually delete) — never delete inside get().

4.FormView and the form-handling methods

FormView processes a plain Form (not tied to a model) — get_form() builds it, form_valid(form) runs on success (default: redirect to success_url), form_invalid(form) runs on failure (default: re-render with errors). CreateView/UpdateView reuse this exact same flow underneath.

Key features

  • get_form(form_class=None) instantiates the form — override to pass extra constructor arguments, like a logged-in user
  • form_valid(form) is called only after form.is_valid() passes — must return an HttpResponse (usually via super(), which redirects to success_url)
  • form_invalid(form) is called when validation fails — the default re-renders the template with the invalid form (and its errors) in context
  • FormView itself does not save anything — it has no model; CreateView/UpdateView layer ModelFormMixin on top for that
  • success_url can be a string, or omitted in favor of get_success_url() for a dynamically computed target

The form-handling methods, in the order they run

The form-handling methods, in the order they run
MethodRuns whenDefault behavior
get_form()always, on GET and POSTinstantiates form_class with request data (if POST)
form.is_valid()called by post() internallyruns field + clean() validation
form_valid(form)validation passedredirect() to success_url
form_invalid(form)validation failedre-render template with the bound, invalid form
FormMixin's fork in the road
TrueFalse

get_form()

always runs, GET and POST

form.is_valid()

form_valid(form)

default: redirect to success_url

form_invalid(form)

default: re-render with errors

  • get_form() — always runs, GET and POST
    • leads to form.is_valid()
  • form.is_valid()
    • leads to form_valid(form) (True)
    • on error, leads to form_invalid(form) (False)
  • form_valid(form) — default: redirect to success_url
  • form_invalid(form) — default: re-render with errors

Command

python
class SignupView(FormView):
    template_name = "signup.html"
    form_class = SignupForm
    success_url = reverse_lazy("login")

    def form_valid(self, form):
        form.save()
        return super().form_valid(form)

form_valid() runs only after validation passes (return super() to keep the redirect); form_invalid() runs on failure (default: re-render with errors) — both must return an HttpResponse like any view method.

5.Mixins, MRO, and when CBVs help vs confuse

A mixin is a small class providing one piece of behavior, meant to be combined via multiple inheritance — Python resolves method calls left-to-right through the class list (MRO), so a mixin must be listed BEFORE the base view class or its methods never get found first.

Key features

  • List a mixin BEFORE the base view class: class MyView(SomeMixin, ListView), never the reverse
  • Python's MRO resolves left to right — the first class (in list order) defining a method wins
  • A mixin combining SingleObjectMixin with ListView needs get_object() called manually in get(), before get_queryset() runs — the two don't coordinate automatically
  • Each of your views should generally use mixins/views from ONE generic-view family (detail, list, editing, date) — the docs' own stated rule
  • When combining families gets complicated, two separate views (dispatched manually, or via separate URLs) are usually clearer than one deeply mixed CBV

Reading a class declaration for MRO order

Reading a class declaration for MRO order
DeclarationChecked first for a methodCorrect when
class MyView(SingleObjectMixin, ListView)SingleObjectMixinthe mixin's methods (get_object) must run before ListView's own
class MyView(ListView, SingleObjectMixin)ListViewalmost never — ListView's get_queryset() runs first, before self.object exists
MRO resolves left to right — first listed, first checked

SingleObjectMixin

listed first — its get_object() wins

ListView

checked next — its get() would otherwise run

View (base)

checked last

  1. SingleObjectMixin — listed first — its get_object() wins
  2. ListView — checked next — its get() would otherwise run
  3. View (base) — checked last

Command

python
class LoginRequiredListView(LoginRequiredMixin, ListView):
    model = Article
    login_url = "/accounts/login/"

List a mixin before the base view class in the parentheses — MRO resolves left to right. Stick to mixins from one generic-view family (detail, list, editing, date) at a time; reach for two separate views before a deeply mixed CBV.

Remember

  • as_view() returns a function urls.py can call; dispatch() (inherited) routes by request.method.
  • TemplateView needs only template_name; ListView adds a queryset — override get_queryset() to filter it.
  • DeleteView deliberately splits GET (confirm) from POST (delete) — never delete inside get().
  • A mixin must be listed before the base view class — Python's MRO resolves left to right.

Pro tip

  • Always call super().get_context_data(**kwargs) first — it is not your context to build from scratch.
  • form_valid() must return super().form_valid(form) (or an equivalent response) to keep the redirect.
  • Stick to mixins from one generic-view family (detail, list, editing, date) — reach for two views before a deeply mixed CBV.

Next up

  • Templates
  • Forms
  • Django Models

Sources

  • Class-based views — docs.djangoproject.com/en/5.2/topics/class-based-views/
  • Generic editing views — docs.djangoproject.com/en/5.2/topics/class-based-views/generic-editing/
  • Every claim checked against Django 5.2 LTS docs · checked 2026-08-21

Spotted a mistake on this page? Tell us.