Class-Based Views (CBVs) are Django's answer to repetitive view code. Instead of writing the same list-filter-paginate or create-validate-save logic in every function-based view, you subclass a generic view that already handles the common pattern and override only what differs.
Here is the honest take: CBVs are not universally better than function-based views (FBVs). They shine when your view fits a well-known pattern — listing objects, showing a detail page, handling a form submission. For those cases, a CBV can reduce a 30-line function to a 5-line class. But for views with complex, non-standard logic — an endpoint that talks to three external APIs and conditionally redirects based on business rules — a plain function is usually clearer. The rule of thumb: if you are fighting the generic view more than using it, switch to an FBV.
Django 6 ships the same CBV hierarchy that has been stable since Django 1.3, but now requires Python 3.12+ and works seamlessly with Django's new Content Security Policy middleware and the async view support introduced in Django 4.1. The fundamentals have not changed: View is the base class, dispatch() routes requests to get()/post() methods, and generic views like ListView and CreateView handle the heavy lifting.
Master django class-based views
Take the Django Foundations course with hands-on lessons and challenges.
ListView handles queryset fetching, pagination, and template rendering. Override get_queryset() to add filtering — never replace the queryset attribute directly when you need request-dependent logic. get_context_data() injects extra variables into the template without touching the main queryset.
DetailView fetches a single object by pk or slug from the URL. Override get_queryset() — not get_object() — to add access control or eager loading. Django calls get_object() internally, which applies the URL kwargs to whatever get_queryset() returns.
CreateView and UpdateView share the same template. form_valid() is the hook for injecting data the user should not control (like the author). Use form_class instead of fields when you need custom validation or widgets. Notice get_queryset() on UpdateView for row-level permissions — if the user does not own the article, Django returns a 404 rather than a 403, which avoids leaking that the object exists.
Mixins are small classes that each do one thing. The order matters: Python's MRO processes left to right, so LoginRequiredMixin runs before OwnerRequiredMixin. Always place mixins before the base view class. Every mixin that overrides a method must call super() to keep the chain intact.
Understanding the method flow is critical for debugging CBVs. setup() is for per-request initialization (added in Django 2.2 to replace __init__ hacks). dispatch() is the central router — override it for logic that applies to all HTTP methods. The HTTP handler methods (get, post, etc.) contain the actual business logic. A new instance is created for every request, so there is no shared mutable state.
Overriding `get_object()` to filter the queryset instead of overriding `get_queryset()`
Override `get_queryset()` and let Django's built-in `get_object()` apply the pk/slug lookup on top of it. This keeps the filtering logic in one place and works correctly with both DetailView and editing views. Reserve `get_object()` overrides for truly unusual lookup patterns.
Setting `queryset = Model.objects.filter(...)` as a class attribute when the filter depends on the request (e.g., `request.user`)
Class attributes are evaluated once at import time, not per request. Use `get_queryset()` for any filtering that depends on the request: `def get_queryset(self): return Article.objects.filter(author=self.request.user)`. The `queryset` attribute is only safe for static filters.
Forgetting to call `super()` in overridden methods like `get_context_data()` or `form_valid()`, which breaks the mixin chain
Always call `super()` and build on top of its return value. For `get_context_data`: `context = super().get_context_data(**kwargs)` then add your keys. For `form_valid`: `return super().form_valid(form)` after your custom logic. Skipping `super()` silently disables every mixin above you in the MRO.
Placing the base view class before mixins in the inheritance list — e.g., `class MyView(ListView, LoginRequiredMixin)`
Mixins must come first: `class MyView(LoginRequiredMixin, ListView)`. Python's MRO resolves methods left to right. If `ListView` comes first, its `dispatch()` runs before `LoginRequiredMixin` gets a chance to check authentication, making the mixin useless.
Django's class-based views eliminate boilerplate for common patterns. ListView, DetailView, CreateView, UpdateView, and DeleteView cover standard CRUD operations with minimal configuration. The key extension points are `get_queryset()` for filtering, `get_context_data()` for template variables, and `form_valid()` for pre-save logic. Mixins add reusable behavior like authentication and permissions — always place them before the base view in the inheritance list and always call `super()`. Use CBVs when your view fits a well-known pattern; reach for function-based views when the generic view fights you more than it helps.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.