Class-Based View Mixins
Reviewed & published by Brayan K
A mixin is a small class that adds one focused piece of behavior to a class-based view through inheritance, so you can compose access checks and extra context onto a view without duplicating code.
Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll use LoginRequiredMixin, PermissionRequiredMixin, and UserPassesTestMixin to guard views, add template data with get_context_data, understand how the MRO orders the dispatch chain, and write your own custom mixin.
What You'll Learn in This Lesson
- Guarding views with LoginRequiredMixin
- Checking permissions with PermissionRequiredMixin
- Custom rules with UserPassesTestMixin
- Adding context with get_context_data and super()
- Why mixin order matters: the MRO
- Writing your own reusable mixin
1 Access Control with LoginRequiredMixin
The most common mixin is LoginRequiredMixin. It works by overriding dispatch — the method that runs before any view handler — to check the user first. If the user is anonymous it redirects to the login page; otherwise it calls super().dispatch() to continue to the real view. You always list it before the base view so its check runs first.
# views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
class DashboardView(LoginRequiredMixin, TemplateView):
template_name = "dashboard.html"
login_url = "/login/" # where to send anonymous users
redirect_field_name = "next" # query param holding the return URL# LoginRequiredMixin works by overriding dispatch(): it checks the user
# FIRST, and only calls the real view if the check passes. This simulates
# that "guard before the view" pattern in plain Python.
class LoginRequiredMixin:
login_url = "/login/"
def dispatch(self, request):
if not request["user"]["is_authenticated"]:
return f"redirect -> {self.login_url}?next={request['path']}"
return super().dispatch(request) # continue to the real view
class View:
def dispatch(self, request):
return self.get(request)
def get(self, request):
return "200 OK: dashboard contents"
class DashboardView(LoginRequiredMixin, View):
pass
view = DashboardView()
anon = {"path": "/dashboard/", "user": {"is_authenticated": False}}
member = {"path": "/dashboard/", "user": {"is_authenticated": True}}
print("anonymous:", view.dispatch(anon))
print("logged in:", view.dispatch(member))
# EXPECTED:
# anonymous: redirect -> /login/?next=/dashboard/
# logged in: 200 OK: dashboard contents
# ✅ Expected output:
# anonymous: redirect -> /login/?next=/dashboard/
# logged in: 200 OK: dashboard contents2 PermissionRequiredMixin and UserPassesTestMixin
For finer control, two more mixins guard a view. PermissionRequiredMixin checks that the user holds a named permission and returns 403 if not. UserPassesTestMixin runs a test_func you write, so you can express any rule — like "only the author may delete this post." Because each mixin calls super().dispatch(), you can stack them and require all the checks to pass.
# views.py
from django.contrib.auth.mixins import (
PermissionRequiredMixin, UserPassesTestMixin,
)
from django.views.generic import DeleteView
from .models import Post
class DeletePostView(PermissionRequiredMixin, UserPassesTestMixin, DeleteView):
model = Post
permission_required = "blog.delete_post"
def test_func(self):
post = self.get_object()
return post.author == self.request.user # author only# PermissionRequiredMixin checks a specific permission; UserPassesTestMixin
# runs a custom test_func() you write. Both deny with 403 if the check fails.
# Here we simulate both guards stacking on one view.
class PermissionRequiredMixin:
permission_required = None
def dispatch(self, request):
if self.permission_required not in request["user"]["perms"]:
return f"403 Forbidden (needs {self.permission_required})"
return super().dispatch(request)
class UserPassesTestMixin:
def test_func(self, request):
raise NotImplementedError
def dispatch(self, request):
if not self.test_func(request):
return "403 Forbidden (failed test_func)"
return super().dispatch(request)
class View:
def dispatch(self, request):
return self.get(request)
def get(self, request):
return "200 OK: deleted the post"
class DeletePostView(PermissionRequiredMixin, UserPassesTestMixin, View):
permission_required = "blog.delete_post"
def test_func(self, request):
# only the author may delete
return request["user"]["id"] == request["post_author_id"]
view = DeletePostView()
author = {"path": "/p/1/del", "user": {"id": 5, "perms": ["blog.delete_post"]}, "post_author_id": 5}
other = {"path": "/p/1/del", "user": {"id": 9, "perms": ["blog.delete_post"]}, "post_author_id": 5}
noperm = {"path": "/p/1/del", "user": {"id": 5, "perms": []}, "post_author_id": 5}
print("author:", view.dispatch(author))
print("other user:", view.dispatch(other))
print("no perm:", view.dispatch(noperm))
# EXPECTED:
# author: 200 OK: deleted the post
# other user: 403 Forbidden (failed test_func)
# no perm: 403 Forbidden (needs blog.delete_post)
# ✅ Expected output:
# author: 200 OK: deleted the post
# other user: 403 Forbidden (failed test_func)
# no perm: 403 Forbidden (needs blog.delete_post)3 get_context_data, the MRO, and Custom Mixins
The other big use of mixins is adding template data through get_context_data. The golden rule is to call super().get_context_data(**kwargs) first, then add your keys. Because the method is cooperative, every mixin in the MRO (method resolution order) gets a chance to contribute. The MRO is simply the left-to-right order Python walks your base classes, and it's why mixin order is so important.
# views.py
from django.views.generic.base import ContextMixin
from django.views.generic import ListView
from .models import Post
class TitleMixin(ContextMixin):
page_title = ""
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) # ALWAYS call super first
ctx["title"] = self.page_title
return ctx
class PostListView(TitleMixin, ListView):
model = Post
page_title = "All Posts"
# The template now gets {{ title }} AND {{ object_list }}# get_context_data and the MRO. A ContextMixin lets each class add to the
# template context by calling super().get_context_data() FIRST, then adding
# its own keys. The MRO (method resolution order) decides the call chain.
class ContextMixin:
def get_context_data(self, **kwargs):
kwargs.setdefault("view", self.__class__.__name__)
return kwargs
class TitleMixin(ContextMixin):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) # cooperative super()
ctx["title"] = "My Page"
return ctx
class SidebarMixin(ContextMixin):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["sidebar"] = ["Home", "About"]
return ctx
class MyView(TitleMixin, SidebarMixin):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["page_id"] = 42
return ctx
# The MRO is the order Python walks the classes:
print("MRO:", " -> ".join(c.__name__ for c in MyView.__mro__ if c is not object))
ctx = MyView().get_context_data()
for key in sorted(ctx):
print(f" {key}: {ctx[key]}")
# EXPECTED:
# MRO: MyView -> TitleMixin -> SidebarMixin -> ContextMixin
# page_id: 42
# sidebar: ['Home', 'About']
# title: My Page
# view: MyView
# ✅ Expected output:
# MRO: MyView -> TitleMixin -> SidebarMixin -> ContextMixin
# page_id: 42
# sidebar: ['Home', 'About']
# title: My Page
# view: MyView🎯 Your Turn: Continue the Chain
A guard mixin must call super() so the dispatch chain reaches the real view. Replace the blank so a staff user gets through.
# 🎯 YOUR TURN: a mixin must call super() so the chain continues to the
# real view. Replace ___ with super().dispatch(request).
class StaffOnlyMixin:
def dispatch(self, request):
if not request["user"]["is_staff"]:
return "403 Forbidden"
return ___ # continue down the chain
class View:
def dispatch(self, request):
return "200 OK: admin panel"
class AdminView(StaffOnlyMixin, View):
pass
print(AdminView().dispatch({"user": {"is_staff": True}}))
# EXPECTED OUTPUT:
# 200 OK: admin panelCommon Errors (And How to Fix Them)
❌ My access mixin doesn't protect the view
You listed the mixin after the base view, so the view runs before the check.
✅ Fix: put access mixins before the base view, e.g. class V(LoginRequiredMixin, TemplateView).
❌ My template lost the object_list / object
Your get_context_data didn't call super, so the generic view's context was dropped.
✅ Fix: start with ctx = super().get_context_data(**kwargs) before adding keys.
❌ TypeError about test_func or NotImplementedError
You used UserPassesTestMixin but never defined test_func.
✅ Fix: implement def test_func(self): returning True/False.
📋 Quick Reference
| Mixin / Method | What It Does |
|---|---|
| LoginRequiredMixin | Redirect anonymous users to the login page |
| PermissionRequiredMixin | Require a named permission, else 403 |
| UserPassesTestMixin | Run a custom test_func returning True/False |
| get_context_data | Add template data (call super() first!) |
| __mro__ | The order Python walks base classes |
🎯 Mini Challenge: Write a Custom Logging Mixin
Write a custom mixin that logs every request, then stack it with a login guard so the chain runs logging, then auth, then the view.
# 🎯 Mini Challenge: write a CUSTOM mixin that logs every request and
# stack it with a login guard. The chain must run: logging -> auth -> view.
class RequestLogMixin:
def dispatch(self, request):
print(f"LOG: {request['user']['name']} hit {request['path']}")
return super().dispatch(request)
class LoginRequiredMixin:
def dispatch(self, request):
if not request["user"]["is_authenticated"]:
return "redirect -> /login/"
return super().dispatch(request)
class View:
def dispatch(self, request):
return self.get(request)
def get(self, request):
return "200 OK: settings page"
class SettingsView(RequestLogMixin, LoginRequiredMixin, View):
pass
view = SettingsView()
print("result:", view.dispatch(
{"path": "/settings/", "user": {"name": "Dana", "is_authenticated": True}}
))
print("---")
print("result:", view.dispatch(
{"path": "/settings/", "user": {"name": "Anon", "is_authenticated": False}}
))
# EXPECTED:
# LOG: Dana hit /settings/
# result: 200 OK: settings page
# ---
# LOG: Anon hit /settings/
# result: redirect -> /login/❓ Frequently Asked Questions
Lesson complete — you can compose views from mixins!
You guarded views with LoginRequiredMixin, PermissionRequiredMixin, and UserPassesTestMixin, added template data with get_context_data and super(), saw how the MRO orders the dispatch chain, and wrote your own custom mixin.
Practice quiz
What is a mixin in class-based views?
- A complete standalone view
- A database model
- A small class that adds one focused behavior via inheritance
- A URL pattern
Answer: A small class that adds one focused behavior via inheritance. A mixin adds one piece of behavior through inheritance rather than being a full view.
Which mixin only checks that the user is logged in?
- PermissionRequiredMixin
- UserPassesTestMixin
- ContextMixin
- LoginRequiredMixin
Answer: LoginRequiredMixin. LoginRequiredMixin redirects anonymous users to the login page.
Where must access-control mixins be listed in the class bases?
- Before the base view class
- After the base view class
- Inside get_context_data
- In urls.py
Answer: Before the base view class. They must come before the base view so their dispatch check runs first in the MRO.
Which mixin returns 403 if the user lacks a named permission?
- LoginRequiredMixin
- PermissionRequiredMixin
- ContextMixin
- TemplateView
Answer: PermissionRequiredMixin. PermissionRequiredMixin checks permission_required and returns 403 Forbidden if missing.
Which mixin runs a custom test_func you write?
- LoginRequiredMixin
- PermissionRequiredMixin
- UserPassesTestMixin
- ContextMixin
Answer: UserPassesTestMixin. UserPassesTestMixin calls your test_func, letting you express any rule.
What is the first thing get_context_data should do?
- Return an empty dict
- Render the template
- Raise NotImplementedError
- Call super().get_context_data(**kwargs)
Answer: Call super().get_context_data(**kwargs). Calling super() first preserves context the parent classes built, like object_list.
What does MRO stand for?
- Method resolution order
- Mixin registration object
- Model request operation
- Multiple route override
Answer: Method resolution order. The MRO is the left-to-right order Python walks base classes for method lookup.
Why must a guard mixin's dispatch call super().dispatch()?
- To log the request
- To continue the chain to the real view
- To hash a password
- To render context
Answer: To continue the chain to the real view. Calling super().dispatch() passes control down the MRO to the actual view.
What happens if get_context_data does not call super()?
- The view is faster
- Permissions are skipped
- Nothing changes
- You lose context like object_list and the template renders blank
Answer: You lose context like object_list and the template renders blank. Skipping super() drops the context the generic view added.
Can you stack several access mixins on one view?
- No, only one is allowed
- Only with permissions disabled
- Yes; the request must pass every guard
- Only on function views
Answer: Yes; the request must pass every guard. Each mixin calls super().dispatch(), so stacked guards must all pass.
Continue this course
- Previous: Formsets & Inline Formsets
- Next: Async Views & ASGI — Write async views and use the async ORM under ASGI
- Quick reference: Django cheat sheet
Frequently asked questions
What is a mixin in Django class-based views?
A mixin is a small class that adds one focused piece of behavior to a view through inheritance, rather than being a complete view on its own. You list mixins before the base view class, and Python combines them. For example LoginRequiredMixin adds an authentication check, and ContextMixin adds extra template context. Because each mixin does one thing, you can mix and match them to compose exactly the behavior a view needs without copying code.
What is the difference between LoginRequiredMixin, PermissionRequiredMixin, and UserPassesTestMixin?
LoginRequiredMixin only checks that the user is logged in and redirects anonymous users to the login page. PermissionRequiredMixin checks that the user holds a specific permission, like blog.delete_post, and returns 403 Forbidden if they don't. UserPassesTestMixin runs a test_func method you write, returning True or False, so you can express any custom rule — such as 'only the author may edit this object.' You can stack several of these on one view to require multiple conditions.
Why does the order of mixins matter?
Because Python uses the method resolution order, or MRO, to decide which class's method runs first when several define the same method. Mixins listed earlier (more to the left) appear earlier in the MRO and run first. Access-control mixins like LoginRequiredMixin must come before the base View so their dispatch check runs before the view executes. Each mixin's method calls super() to continue the chain, so getting the order wrong can skip a check or break context building.
How do I add extra data to a class-based view's template context?
Override get_context_data. The crucial step is to call super().get_context_data(**kwargs) first to get the context the parent classes built, then add your own keys and return the dictionary. Because get_context_data is cooperative, every mixin and the view itself can contribute keys as the call travels up the MRO. If you forget to call super(), you lose the context other classes added, such as the object or object_list a generic view provides.