A Django class-based view (CBV) is a view written as a Python class instead of a function. Django turns the class into a normal view callable, then routes each HTTP method (GET, POST, and so on) to a matching method on the class (get(), post(), ...). Because views become classes, you can reuse behaviour through inheritance and mixins instead of copying code between functions.
When should you reach for a CBV? Use one when your view maps cleanly onto a common pattern — rendering a template, listing objects, showing one object, or handling create/update/delete forms — because Django's generic CBVs already implement that flow for you. Reach for a function-based view (FBV) when the logic is short, linear, and bespoke, where a class adds ceremony without saving anything.
This guide covers how a CBV works under the hood, the generic views you will actually use day to day, mixins, URL wiring, and the cases where an FBV is still the right call — all using current Django 5.x and Python 3.12+ conventions. We at MicroPyramid have built and maintained Django applications for 12+ years across 50+ projects, and the patterns below are the ones we apply in production.
Function-based views vs class-based views
Both styles produce the same thing — a callable that takes a request and returns a response. The difference is how you organise and reuse the logic. An FBV branches on request.method inside one function; a CBV gives each method its own handler and lets you share code through inheritance.
Here is the same "hello world" view in both styles:
# Function-based view
from django.http import HttpResponse
def hello_world(request):
if request.method == "GET":
return HttpResponse("Hello, World!")
return HttpResponse(status=405)
# Class-based view (equivalent)
from django.http import HttpResponse
from django.views import View
class HelloWorldView(View):
def get(self, request, *args, **kwargs):
return HttpResponse("Hello, World!")Notice the CBV never checks request.method itself. If a POST arrives, Django sees there is no post() method and automatically returns a 405 Method Not Allowed — you get that behaviour for free.
| Aspect | Function-based view (FBV) | Class-based view (CBV) |
|---|---|---|
| HTTP method handling | Manual if request.method == ... branches |
One method per verb: get(), post(), delete() |
| Code reuse | Decorators and helper functions | Inheritance and mixins |
| Common CRUD patterns | Write the flow yourself | Generic views implement it for you |
| Readability for simple logic | Excellent — top to bottom | Indirection across base classes |
| Customisation hooks | Wherever you write them | Named overrides (get_queryset, get_context_data, ...) |
| Best fit | Short, bespoke, linear logic | Repetitive CRUD and list/detail pages |
How a class-based view works under the hood
A URL pattern expects a callable, but a class is not a callable view on its own. The bridge is the classmethod as_view(), which every View subclass inherits. Calling MyView.as_view() returns a thin function that, on each request, creates a fresh instance of your view and calls its dispatch() method. Using a new instance per request is important: it keeps per-request state (like self.request) from leaking between concurrent requests.
dispatch() is where routing happens. It looks at request.method, lower-cases it, and checks whether your class defines a matching handler that is also listed in http_method_names (the default is get, post, put, patch, delete, head, options, trace). If a handler exists, dispatch() calls it; if not, it calls http_method_not_allowed(), which returns an HttpResponseNotAllowed (status 405) listing the methods your view does support.
So the journey of a GET request is: as_view() returns the view function → Django calls it → it instantiates the view and calls dispatch() → dispatch() routes to get() → get() returns the response. Because dispatch() is just a method, you can override it to add behaviour that should run for every HTTP method — logging, feature flags, or a permission check (though mixins are usually cleaner for that).
from django.http import HttpResponse
from django.views import View
class GreetingView(View):
# Restrict which verbs this view answers (optional).
http_method_names = ["get", "post"]
def dispatch(self, request, *args, **kwargs):
# Runs for every allowed method before get()/post().
# Good place for cross-cutting logic; call super() to continue.
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
return HttpResponse("Hello via GET")
def post(self, request, *args, **kwargs):
return HttpResponse("Hello via POST")The View base class
Every class-based view ultimately inherits from django.views.generic.base.View (re-exported as django.views.View). It is deliberately minimal: it provides as_view(), dispatch(), http_method_not_allowed(), and a default options() handler. It does not know anything about templates, models, or forms.
Subclass View directly when you want full control and none of the generic machinery — for example, a JSON endpoint, a webhook receiver, or a redirect that depends on custom logic. For anything that renders a template or talks to a model, prefer the generic views below, which build on View and save you a lot of code.
Generic display views: TemplateView, ListView, DetailView
Django ships generic class-based views that implement the most common read-only flows. You set a couple of attributes and override a hook or two; Django handles the rest.
TemplateViewrenders a template with a context. Use it for static-ish pages (about, dashboard shells) where you do not need a model.ListViewfetches a queryset and renders a list. It paginates, sets a context variable (object_list, plus a friendlier<model>_list), and uses a default template name like<app>/<model>_list.html.DetailViewfetches a single object by primary key or slug from the URL and renders it, using a default template like<app>/<model>_detail.html.
The two hooks you will override most are get_queryset() (to filter or order what is shown) and get_context_data() (to add extra variables to the template).
from django.views.generic import TemplateView, ListView, DetailView
from .models import Article
class HomeView(TemplateView):
template_name = "blog/home.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["featured"] = Article.objects.filter(is_featured=True)[:3]
return context
class ArticleListView(ListView):
model = Article
paginate_by = 10
context_object_name = "articles" # template uses {{ articles }}
template_name = "blog/article_list.html"
def get_queryset(self):
# Only show published articles, newest first.
return Article.objects.filter(published=True).order_by("-created_at")
class ArticleDetailView(DetailView):
model = Article
context_object_name = "article"
template_name = "blog/article_detail.html"
# Looks up by <pk> or <slug> captured in the URL pattern.Generic editing views: FormView, CreateView, UpdateView, DeleteView
The editing views handle form display, validation, saving, and redirection — the parts of CRUD that are tedious to write by hand. If you are new to Django forms, our Django forms basics guide covers the Form/ModelForm foundations these views build on.
FormViewdisplays aForm, validates it onPOST, and on success runsform_valid()and redirects tosuccess_url. Use it when there is no single model to save (e.g. a contact form that sends an email).CreateViewbuilds aModelFormfrom yourmodel/fields, saves a new object on valid submission, and redirects.UpdateViewisCreateViewfor an existing object — it loads the instance from the URL, pre-fills the form, and saves changes.DeleteViewshows a confirmation page onGETand deletes the object onPOST, then redirects tosuccess_url.
With CreateView/UpdateView you usually set model, fields (or a custom form_class), and success_url (or define get_success_url()).
from django.urls import reverse_lazy
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from .models import Article
class ArticleCreateView(CreateView):
model = Article
fields = ["title", "body", "published"]
template_name = "blog/article_form.html"
success_url = reverse_lazy("article-list")
def form_valid(self, form):
# Attach the logged-in user before saving.
form.instance.author = self.request.user
return super().form_valid(form)
class ArticleUpdateView(UpdateView):
model = Article
fields = ["title", "body", "published"]
template_name = "blog/article_form.html"
success_url = reverse_lazy("article-list")
class ArticleDeleteView(DeleteView):
model = Article
template_name = "blog/article_confirm_delete.html"
success_url = reverse_lazy("article-list")Mixins: composing behaviour
Mixins are small classes you combine with a view to add a slice of behaviour. Because Python supports multiple inheritance, you list the mixin before the base view so its method resolution order (MRO) takes precedence.
LoginRequiredMixin(fromdjango.contrib.auth.mixins) redirects anonymous users to the login page before the view runs. There is alsoPermissionRequiredMixinfor permission checks andUserPassesTestMixinfor custom rules.ContextMixinis what suppliesget_context_data(); every template-rendering generic view inherits it, which is why overridingget_context_data()works everywhere.- Your own mixins can override
get_queryset()orget_context_data()to share filtering or context logic across several views.
The golden rule with the auth mixins: put them first in the inheritance list so their checks happen before the view's main logic.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from .models import Article
# A reusable mixin that scopes the queryset to the current user.
class OwnedQuerysetMixin:
def get_queryset(self):
return super().get_queryset().filter(author=self.request.user)
class MyArticleListView(LoginRequiredMixin, OwnedQuerysetMixin, ListView):
model = Article
template_name = "blog/my_articles.html"
login_url = "/accounts/login/" # where anonymous users are sent
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "My articles"
return contextWiring CBVs into URLs with path() and .as_view()
In urls.py you reference the view through as_view(). In modern Django you use path() (and re_path() for regular expressions) — the old django.conf.urls.url() was removed in Django 4.0, so do not use it.
Pass URL converters such as <int:pk> or <slug:slug> in the route; DetailView, UpdateView, and DeleteView read them automatically. You can also pass per-view configuration as keyword arguments to as_view() itself, which is handy for reusing one view with small variations.
from django.urls import path
from . import views
urlpatterns = [
path("", views.HomeView.as_view(), name="home"),
path("articles/", views.ArticleListView.as_view(), name="article-list"),
path("articles/new/", views.ArticleCreateView.as_view(), name="article-create"),
path("articles/<int:pk>/", views.ArticleDetailView.as_view(), name="article-detail"),
path("articles/<int:pk>/edit/", views.ArticleUpdateView.as_view(), name="article-update"),
path("articles/<int:pk>/delete/", views.ArticleDeleteView.as_view(), name="article-delete"),
# Reuse one view with different attributes via as_view() kwargs:
path("drafts/", views.ArticleListView.as_view(
queryset=None, template_name="blog/drafts.html"
), name="draft-list"),
]When a function-based view is still the better choice
CBVs shine when your view matches a generic pattern, but they are not always the right tool. Prefer an FBV when:
- The logic is short and linear — a handful of lines that read top to bottom. A class adds indirection without saving code.
- The view does something bespoke that no generic view models well, so you would override most of the class anyway.
- You want the flow to be obvious to newcomers. A single function is easier to follow than tracing methods up an inheritance chain.
- You are writing a one-off endpoint — a webhook, a health check, a small AJAX handler.
A good heuristic: start CRUD and list/detail pages as generic CBVs, and write everything else as FBVs until repetition tells you to refactor. If you already have a codebase of function views and want to move the repetitive ones to classes, see our walkthrough on migrating from function-based views to class-based views. And if you are building APIs, the same CBV ideas extend into Django REST Framework — start with our introduction to API development with Django REST Framework.
Frequently Asked Questions
What is a class-based view in Django?
A class-based view is a Django view written as a Python class that subclasses django.views.View (or one of the generic views). Django calls the class's as_view() method to turn it into a normal view callable, then routes each HTTP method to a matching handler such as get() or post(). The main benefit over function-based views is code reuse through inheritance and mixins.
What does as_view() do?
as_view() is a classmethod that returns a function Django can use as the view in your URL pattern. On every request that function creates a fresh instance of your view class and calls its dispatch() method, which routes the request to the correct HTTP-method handler. A new instance per request keeps per-request data from leaking between concurrent requests.
How does dispatch() route a request?
dispatch() reads request.method, lower-cases it, and checks whether your class has a matching handler that is also listed in http_method_names. If a handler like get() or post() exists, dispatch() calls it; otherwise it calls http_method_not_allowed(), which returns a 405 Method Not Allowed response listing the supported methods.
When should I use a class-based view instead of a function-based view?
Use a class-based view when your view fits a common pattern that Django's generic views already implement — rendering a template, listing objects, showing a detail page, or handling create/update/delete forms. Use a function-based view when the logic is short, linear, and bespoke, where a class would add indirection without reducing code.
What are mixins and why does their order matter?
Mixins are small classes that add a single slice of behaviour, such as LoginRequiredMixin for authentication. You combine them with a view through multiple inheritance. Order matters because Python resolves methods left to right (the MRO), so auth mixins must come before the base view in the class definition for their checks to run first.
Should I use url() or path() to wire up class-based views in Django 5.x?
Use path() (and re_path() for regular expressions). The older django.conf.urls.url() was removed in Django 4.0 and does not exist in Django 5.x. You still call .as_view() on the class, for example path("articles/<int:pk>/", ArticleDetailView.as_view(), name="article-detail").