Vincent A Saulys' Blog
Django CBVs: All the Gnary Bits
Tags: django cheatsheet
January 30, 2021

Web frameworks largely come in two flavors: micro and "batteries-included." Django falls in the latter category, making many assumption on how to write and structure your code. You can break these conventions but it will involve a lot of pain.

However, Django remains uniquely flexible in how you write your views. You can use functions or classes to do this. Most beginners will use function based views (FBVs) as they're easier to reason about. Your request comes into the function, gets processed, and a response is returned.

Class based views (CBVs) are far more gnarly. It's not clear on first glance what your request does or where it goes. But understanding CBVs will massively improve productivity. You'll stop writing redundant code and be able to leverage a lot of pre-built scaffolding. This lets you write faster and keeps your code far more readable, limiting technical debt in the future.

The catch is that you have to grok CBVs.

Why Function Based Views don't always cut it.

Function Based Views (FBVs) are the first way you'll probably learn to write django views.

Requests come in as an object, they get processed, a response is returned.

def my_cool_view(request):
    # ... some processing logic
    return render(request, "my_cool_view_template.html", context)

They do work well but where things get complicated.

Let's suppose you want to start protecting views from unauthenticated users. You'll rummage through the docs and discover the django.contrib.auth.decorators.login_required decorator and attach it to the top of a function. Simple enough.

@login_required
def my_cool_view(request):
    # alternatively you could use `if request.user.is_authenticated: ...`
    # ... some processing logic
    return render(request, "my_cool_view_template.html", context)

But what if you want to block off a page to only users who own it?

Now you can build in the logic via an if-else statement within the FBV. But this looks messy. The logic for processing the request is now mixed with the authentication checking.

@login_required
def my_cool_view(request):
    if my_object.user == request.user:
        # ... some processing logic
        return render(request, "my_cool_view_template.html", context)
    return HttpResponseForbidden()  # from django.http

What if we want to handle multiple types of requests to a given route? You'll need to start adding checking with if-else statements within the function. Again, the logic is being muddled. Your one function is handling a lot of work.

It would be better to separate it out so that we have multiple functions handling multiple parts of the logic. This would vastly improve readability.

@login_required
def my_cool_view(request):
    if request.method == 'GET':
        if my_object.user == request.user:
            # ... some processing logic
            return render(request, "my_cool_view_template.html",context)
    elif request.method == 'POST':
        if my_object.user == request.user:
            # ... different processing logic
            return redirect( "detail-view", pk = obj.pk)
    return HttpResponseForbidden()  

As this goes on, the function will become more and more difficult to read. You could add lots of comments to try and remind yourself. That would make the code even more lengthy and verbose. [0]

Class based views help alleviate this problem.

Let's use the previous snippet of code and refactor is as a CBV.

class MyCoolView(LoginRequiredMixin, UserPassesTestMixin, View):

    def get(self, request):
        # some process logic
        return render(request, "my_cool_view_template.html",context)

    def post(self, request):
        # ... different processing logic
        return redirect( "detail-view", pk = self.obj.pk)

    def test_func(self):
        return self.get_obejct().user == self.request.user

    def get_object(self):
        # get the object out of database -- more on this later
        return obj

This looks much better. We separated out the concerns here so the get and post requests are handled separately. We pull in UserPassesTestMixin to open up test_func to check if the user owns the object in question. Lastly, we separate out the logic to get the object out of database so that we can use it in both our get and post functions and avoid rewriting the code that twice.

This only touches the beginning of class based views. Django includes a number of built-in mixins and generic objects to massively speed up writing applications. Once you learn how to leverage them, you'll increase your coding skills helping you get your project up and running much quicker.

Let's dig in.

Mixins as the roots of CBVs

We'll need to start with some background on Python's intricacies in inheritance.

Class based views in Django leverage mixins. This is how Python handles multiple inheritance. You can think of it as an interface if you're coming from a Java background.

This is a deep topic. But for our purposes, all you really need to remember is that inheritance overloads from the left-to-right.

Suppose we have an object with basic function def foo. We then write another object with the same def foo function. In a third object, we inherit from both and call foo(). What happens?

class obj1(object):
    def foo(self):
        print("foo from obj1")


class obj2(object):
    def foo(self):
        print("foo from obj2")


class obj3(obj1, obj2):
    def __init__(self):
        pass


my_cool_object = obj3()
my_cool_object.foo()

That answer depends on the order of classes in obj3. If we include obj1 first, the def foo from obj1 will be called.

$ python3 examples.py 
foo from obj1

But if we swap the order, we'll get the other def foo function from obj2 instead.

# ...

class obj3(obj2, obj1):
    def __init__(self):
        pass

my_cool_object = obj3()
my_cool_object.foo()
# >>> "foo from obj2

Mixins come into play when we start calling the parent class function inside our overloaded method. We do this using super().

Python is an interpreted language and doesn't check if there is the corresponding function in the parent class ahead of time. It will only check on evaluation. This means we can write obj2 to call a super function that doesn't exist in its parent class.

class obj1(object):
    def foo(self):
        print("foo from obj1")


class obj2(object):
    def foo(self):
        print("foo from obj2")
        super().foo()  # NEW ADDITION


class obj3(obj2, obj1):
    def __init__(self):
        pass


my_cool_object = obj3()
my_cool_object.foo()

What's going on here?

obj2 calls the parent class' def foo function. It's own parent class doesn't have one but the parent class of obj3 does, triggering obj1.foo().

$ python3 examples.py 
foo from obj2
foo from obj1

Remember, the functions are being read left to right and the right most object is the "most senior" parent class in the call chain.

Note that attempting to subclass obj2 directly and call foo will result in an error being raised.

$ python3 examples.py 
foo from obj2
Traceback (most recent call last):
  File "examples.py", line 18, in <module>
    my_cool_object.foo()
  File "examples.py", line 9, in foo
    super().foo()
AttributeError: 'super' object has no attribute 'foo'

Lazy evaluation also means that these "primitive" mixins can rely on a constant that doesn't yet exist but be expected in the child class that inherits multiple parents. [1]

class obj1(object):
    def foo(self):
        print("foo from %s" % self.my_subclassed_constant)


class obj2(obj1):
    my_subclassed_constant = "obj2"


my_cool_object = obj2()
my_cool_object.foo()
# >>> foo from obj2

Mixins are key to understanding CBVs. Django creates a lot of mixin "primitives" that do very specific functions but are useless on their own. Instead, you combine them to construct your CBVs.

With that said, let's look at the first CBVs available.

Basic CBVs -- TemplateView, DetailView, ListView

Under django.views.generic lie the first three CBVs we'll be looking at:

TemplateView is the most straightforward. Provide a template_name and insert into urls. At that url, it will return a rendered template. Simple enough.

# views.py
from django.views.generic import ListView, DetailView, TemplateView

class MyTemplateview(TemplateView):
    # note this resolves as "templates/<app-name>/my_template_name.html"
    template_name = "my_template_name.html"  


# urls.py
from django.urls import path, include

from .views import *

urlpatterns = [
    path("", MyTemplateView.as_view(), name="my-template-view"),
    # alternatively,
    # path("", MyTemplateView.as_view(template_name="my_template_name.html"), name="my-template-view"),
]

But what if we want to add context to our rendered template? that's simple enough. TemplateView uses the ContextMixin to provide two ways of adding extra context. We can pass a dictionary to the object variable extra_context or override def get_context_data(self, **kwargs).

# METHOD 1: passing an optional dictionary to `extra_context`
# views.py
from django.views.generic import ListView, DetailView, TemplateView

class MyTemplateview(TemplateView):
    template_name = "my_template_name.html"
    extra_context = {"more_data": "some more information"}

# METHOD 2: overriding `def get_context_data(self, **kwargs))`
# views.py
from django.views.generic import ListView, DetailView, TemplateView

class MyTemplateview(TemplateView):
    template_name = "my_template_name.html"

    def get_context_data(self, **kwargs):
        '''should return a dictionary'''
        context = super().get_context_data(**kwargs)
        context["more_data"] = "some more information"
        return context

In both cases, we can now call {{ more_data }} within our rendered template <app-name>/my_template_name.html.

Notice the separation of concerns here. We have one function dedicated to getting extra context for our template. That improves readability as we only have to look at that function to figure out what context is getting passed to the template.

Also note that, in the second example, we're calling super(). This calls the parent class' get_context_data, which is preprocessing other context data. This will be important later on when we're dealing with editing classes.

Let's take a look at DetailView next.

# views.py
from django.views.generic import ListView, DetailView, TemplateView

from .models import Quip

class QuipDetailView(DetailView):
    model = Quip

# urls.py
from django.urls import path, include

from quips.views import *

urlpatterns = [
    path("c/<int:pk>", QuipDetailView.as_view(), name="quip_detail"),
]

This looks incomplete at first glance. We only added added a self.model variable to our CBV. What's going on?

DetailView handles all the logic of finding an object and rendering the template. It looks for a default template under <app-name>/<model-name>_detail.html. You can access the model Quip with all its attributes under {{ object }} according to its primary key passed to the url under pk. [2]

This is powerful. Instead of fussing about building a function that fetches the correct object, finds a particular template, and then fuses the two together, DetailView does all that for us and we only need to tell it which model to use.

These defaults may not be ideal though. Perhaps we want our template to have a different name, our model to be rendered in the template as something other than {{ object }}, or to name our primary lookup url parameter as something other than pk. We can replace these all easily:

class QuipDetailView(DetailView):
    model = Quip
    context_object_name = "quip"  # replaces {{ object }} in the template
    pk_url_kwarg = "_id"          # replaces `pk` in the `urlpatterns`
    template_name = "new_quip_detail.html"  # replaces "quip_detail.html"

Under the hood, DetailView is using SingleObjectMixin to call def get_object(self, queryset=None). This function will use either the constant self.queryset or def self.get_queryset(). In both cases, it expects a queryset object from Django's ORM.

We can opt to provide only only the self.queryset constant. This allows us to query for specific attributes.

class QuipDetailView(DetailView):
    model = Quip
    queryset = Quip.objects.values("content", "user.username")

Alternatively, we can replace the def get_object or def get_queryset functions directly. This lets us be more dynamic.

class QuipDetailView(DetailView):
    model = Quip

    def get_queryset(self):
        # self.kwargs holds the passed url parameters like `pk`
        filter_by = self.kwargs['filter_by']  
        return Quip.objects.filter(content_icontains=filter_by)

# urls.py
from django.urls import path, include

from quips.views import *

urlpatterns = [
    path("c/<int:pk>/<str:filter_by>", QuipDetailView.as_view(), name="quip_detail"),
]

What about return multiple objects instead of a specific one for display in a template?

That would involve the django.views.generic.ListView. This works identical to DetailView but will not look up an object by pk. Instead it passes all the objects found as a list of dictionaries to the template under {{ objects }}.

from django.views.generic import ListView

class QuipDetailView(DetailView):
    model = Quip

We can tweak the specific queryset pased just like before, modifying the def get_queryset function and self.queryset variable.

(Your next thought is probably "How do I prevent authorized users from viewing this?" which is explained later)

For now, let's trudge forward into editing classes. This will let us to send post and get requests via forms.

Editing CBVs -- CreateView, UpdateView, DeleteView

These fall under django.views.generic.edit:

The names are straightforward to comprehend. They all subclass django.views.generic.edit.FormView.

You don't actually need to pass in a django.forms.Form object. You can opt to pass in a self.model variable and a list of self.fields to include in the form. It will use defaults when rendering the fields in a template (e.g. text boxes for CharField).

Of course, you can pass in a form object too. This is typically done when rendering custom fields, using third party libraries like django-crispy-forms, or you're converting pre-existing forms to CBVs.

If you pass both a form_class and model + fields, then Django will raise an exception. [3]

CreateView processes the def get and def post functions. It excepts a URL with no parameters. If you send a GET request, it will return a rendered template under <app-name>/<model-name>_form.html and if you send a POST request, it will create a new object or return an error if the form doesn't validate.

UpdateView will expect a URL with a pk parameter. If given a GET request, it will return the template <app-name>/<model-name>_form.html. If you send a POST request, it will look up the object by its primary key and update it with the values passed. If it successfully updates, it will redirect to the success_url variable.

DeleteView will expect a URL with a pk parameter. If given a GET request, it will return the template <app-name>/<model-name>_confirm_delete.html where you usually ask the user if they really want to delete. If given a POST request, it will delete the object at pk. Upon successful deletion, it will redirect to the success_url variable.

To simplify understanding this, I've included a the view objects, the url patterns, and skeleton html templates, passing in the defaults:

# views.py
class QuipCreateView(CreateView):
    model = Quip
    fields = ["content"]
    success_url = reverse_lazy("quip_list")
    context_object_name = "object"
    template_name = "quip_form.html"  


class QuipUpdateView(UpdateView):
    model = Quip
    fields = ["content"]
    success_url = reverse_lazy("quip_list")
    pk_url_kwarg = "pk"
    template_name = "quip_form.html" 


class QuipDeleteView(DeleteView):
    model = Quip
    success_url = reverse_lazy("quip_list")
    pk_url_kwarg = "pk"
    template_name = "quip_confirm_delete.html"

# urls.py
from django.urls import path, include

from quips.views import *

urlpatterns = [
    path("c/create_quip/", QuipCreateView.as_view(), name="quip_create"),
    path("c/update_quip/<int:pk>", QuipUpdateView.as_view(), name="quip_update"),
    path("c/delete_quip/<int:pk>", QuipDeleteView.as_view(), name="quip_delete"),
]
<!-- quip_form.html -->

<div>
  <!-- action="<same-url-GET-if-not-mentioned>" -->
  <form method="POST">
    {% csrf_token %}
    {{ form.content }}
    <button type="submit">Submit New Quip</button>
  </form>
</div>

<!-- quip_confirm_delete.html -->

<form method="POST">
  {% csrf_token %}
  <button type="submit">Delete Quip?</button>
</form>

There is still a bit more we can do. Now that we have the basic objects under our belt, let's look at overriding certain functions and using mixins to save ourself repeat code writes.

LoginRequiredMixin and UserPassesTestMixin

Django features many mixins, which we've used above. There are two "utility" mixins that we can opt to include.

One is LoginRequiredMixin. It's usage is straight forward. It will check that the user in question is logged in before returning the view, other it will 403. This allows us to assume a request.user object exists so we don't have to look for it with if request.user: ... statements.

Two is UserPassesTestMixin. This will look for a function def test_func(self) and check for a boolean return. If true, it will return the view as normal. If false, it will raise a 403.

Let's go back to our DeleteView we wrote earlier. We don't want users willy-nilly deleting objects. We also don't want users who don't own the particular object deleting them. We can use both LoginRequiredMixin and UserPassesTestMixin to do this.

# views.py
class QuipDeleteView(UserPassesTestMixin, LoginRequiredMixin, DeleteView):
    model = Quip
    success_url = reverse_lazy("quip_list")

    def test_func(self):
        """Check that the user owns this quip"""
        return self.get_object().user == self.request.user

You'll notice a couple things going on here.

One is that we have three parent classes. Remember that we evaluate from left-to-right which means that the object will attempt to evaluate a funtion under UserPassesTestMixin first before going to LoginRequiredMixin and then to DeleteView. This doesn't matter much here as there isn't any overloaded functions but it will later.

Two is that we have a def test_func function defined. That's what UserPassesTestMixin expects. Knowing that we can safely expect a self.user, we can then check if the object requested is owned by that user. I'll include the model code below to make this clear.

# models.py
from django.db import models
from django.contrib.auth.models import User


class Quip(models.Model):
    # pk => primary key will be automatic
    content = models.CharField(max_length=360)
    user = models.ForeignKey(User, on_delete=models.CASCADE)

Perhaps a bit more obtuse is the self.get_object() call. This is not stated directly in the docs. You need to glean it from one of its parent mixins. In this case, that is django.views.generic.detail.SingleObjectMixin.

This approach can be expanded on down for all of the views. But that would take a lot of time and violates DRY principles. Better is to create your own mixins.

Building your own Mixins to not repeat yourself

The following mixins are ones I commonly use. It's best to include these in a separate <app-name>/mixin.py file for cleaner reading.

Here is the deletion mixin but refactored as a mixin.

from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin


class OwnedByUserMixin(UserPassesTestMixin, LoginRequiredMixin):
    def test_func(self):
        """Check that the user owns this quip"""
        return self.get_object().user == self.request.user


# views.py
class QuipDeleteView(OwnedByUserMixin, DeleteView):
    model = Quip
    success_url = reverse_lazy("quip_list")

Next is a mixin to assign any created object to the logged in user who made a request. The def form_valid is called to validate a form. We can "intercept" it and attach the user information before calling the parent class' form validation.

class AssignToUserMixin(LoginRequiredMixin):
    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

# views.py
class QuipCreateView(AssignToUserMixin, CreateView):
    # quip_form.html
    model = Quip
    fields = ["content"]
    success_url = reverse_lazy("quip_list")

This mixin is effectively an interface. As discussed before, Python will not check if super().form_valid(form) is valid under runtime. We know that def form_valid is defined under django.views.generic.edit.FormMixin which is part of the FormView that CreateView, UpdateView, and DeleteView all use. The overridden function reads left-to-right, looking in AssignToUserMixin, LoginRequiredMixin, then CreateView.

Next is a mixin to add the logged in user information as template context. It's nice for user pages for getting the logged in user's name.

class AddUserContextData(LoginRequiredMixin):
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["user"] = self.request.user
        return context

Lastly is a mixin to handle ajax requests. This will return a JSON response instead of HTML when AJAX is used.

class JsonResponseMixin(object):
    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)
        if request.is_ajax():
            return JsonResponse({"url_redirect": self.get_success_url()}, status=200)
        return response

Some example jQuery that can use is below.

$('button').on('click', function(e) {
  e.preventDefault();
  console.log("clicked");
  $.post(
    "{% url 'quip_create' %}",
    $('form').serialize(),
    function(resp) {
      console.log("success!", resp);  // could also put in a loading element
      window.location.replace("/");
    }
  )
});

User Authentication System

Django's built in user authentication system is one of the key differences with Ruby-on-Rails.

It handles a lot of the basic cruft you'll have. It will hash passwords, check for valid emails and usernames, and provide an administrative GUI.

It also includes a set of pre-built URLs to use. Things like login, logout, password changes and resets. This is all boilerplate and largely the same from project to project. Providing it upfront is very helpful.

You could include this within the same app but as Django does not provide user sign up or profile default views, I like to create a separate accounts app.

To include these default urls, simply include the following.

# urls.py
from django.urls import path, include


urlpatterns = [
    path("", include("django.contrib.auth.urls")),
]

This is not much code to write but it opens up a host of default routes to use. Each corresponds to a particular default CBV which will look for a default template. If you use only the defaults, highly recommended when starting a project, this will be:

Alternatively, you could write the following and overwrite/extend as deemed necesary:

# urls.py
from django.urls import path, include
from django.contrib.auth.views import (
    LoginView,
    LogoutView,
    PasswordChangeDoneView,
    PasswordResetView,
    PasswordResetConfirmView,
    PasswordResetCompleteView
)


urlpatterns = [
    path("login/", LoginView.as_view(), name="login"),
    path("logout/", LogoutView.as_view(), name="logout"),
    path("password_change/", PasswordChangeView.as_view(), name="password_change"),
    path("password_change/done/", PasswordChangeDoneView.as_view(), name="password_change_done"),
    path("password_reset/", PasswordResetView.as_view(), name="password_reset"),
    path("reset/<uidb64>/<token>/", PasswordResetConfirmView.as_view(), name="password_reset_confirm"),
    path("reset/done/", PasswordResetCompleteView.as_view(), name="password_reset_complete"),
]

Django includes default templates for all of these. I've included a few snippets to use as skeletons below.

<!-- registration/login.html -->
<h1>Login Page</h1>
<div>
  <a href="{% url 'password_reset' %}">Reset password via email</a>
</div>
<div>
  <form action="{% url 'login' %}" method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Submit</button>
  </form>
</div>


<!-- registration/logged_out.html -->
<h1>You have succesfully Logged Out</h1>


<!-- registration/password_change_form.html -->
<h1>Change Password you already know</h1>
<form method="POST"> <!-- will send to the same URL -->
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" value="Change Password">
</form>


<!-- registration/password_change_done.html -->
<h1>Password Reset Complete</h1>


<!-- registration/password_reset_form.html -->
<h1>Forgot your password?</h1>
<form method="POST"> <!-- will send to the same URL -->
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" value="Email me next steps">
</form>


<!-- regisration/password_reset_confirm.html -->
<h1>Change password you don't know</h1>
<form method="POST">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" value="Change Password">
</form>


<!-- registration/password_reset_done.html -->
<h1>You're password has been reset</h1>

Wrap-Up

Django is a mighty complex machine and you'll uncover all sorts of "I didn't know you could do this" if you start looking through the documentation.

The catch is knowing enough to get running. Class Based Views fall under that.

Once you know which ones to go for in what situations, you'll be doing well.

To help with that, I've created a "cheat sheet" with the most common CBVs and some useful mixins. It's available as a PDF here or in HTML form below.

And if you like this post, subscribe to my newsletter! I'll let you know whenever new posts on Django and machine learning come out.

import django


class _(django.views.generic.TemplateView):
    template_name = "_"
    extra_context = {"key": "value"}

    def get_context_data(self, **kwargs):
        """django.views.generic.base.ContextMixin"""
        context = super().get_context_data(**kwargs)
        # context['other_data'] = 'some other data'  # str or list
        return context


class _(django.views.generic.DetailView):
    model = None
    template_name = "<model-name>_detail.html"
    queryset = None
    pk_url_kwarg = 'pk'
    context_object_name = "object"

    def get_queryset(self, **kwargs);   
    def get_object(self, queryset=None);
    def get_context_data(self, **kwargs);


class _(django.views.generic.ListView):
    model = None
    template_name = "<model-name>_list.html"
    context_object_name = "object_list"
    paginate_by = None   

    def get_queryset(self, **kwargs);
    def get_context_data(self, **kwargs);


class _(django.views.generic.edit.FormView):
    template_name = None
    form_class = None
    model = None      # either model+fields or form_class must be passed
    fields = []
    success_url = None  # reverse_lazy('pattern-name')

    def form_valid(self, form);


class _(django.views.generic.edit.CreateView):
    template_name = '<model-name>_form.html'
    form_class = None
    model = None      # either model+fields or form_class must be passed
    fields = []
    success_url = None  # reverse_lazy('pattern-name')

    def form_valid(self, form);


class _(django.views.generic.edit.UpdateView):
    template_name = '<model-name>_form.html'
    form_class = None
    model = None      # either model+fields or form_class must be passed
    fields = []
    success_url = None  # reverse_lazy('pattern-name')

    def form_valid(self, form);


class _(django.views.generic.edit.DeleteView):
    template_name = '<model-name>_confirm_delete.html'
    model = None      # either model+fields or form_class must be passed
    success_url = None  # reverse_lazy('pattern-name')

    def get_success_url(self);   # return success_url by default


class _(django.views.generic.base.RedirectView):
    url = None
    pattern_name = None  # lazy eval url pattern
    permanent = False    # 301 if True, 302 if False
    query_string = False # appends query string to new location if True

    def get_redirect_url(self, *args, **kwargs); # args and kwargs from URL pattern


# ---- some useful mixins

class OwnedByUserMixin(
        django.contrib.auth.mixins.UserPassesTestMixin,
        django.contrib.auth.mixins.LoginRequiredMixin):
    def test_func(self):
        """Check that the user owns this quip"""
        return self.get_object().user == self.request.user


class AssignToUserMixin(
        django.contrib.auth.mixins.LoginRequiredMixin):
    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)


class AddUserContextData(
        django.contrib.auth.mixins.LoginRequiredMixin):
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["user"] = self.request.user
        return context


class JsonResponseMixin(object):
    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)
        if request.is_ajax():
            return django.http.JsonResponse({"url_redirect": "/"}, status=200)
        return response

Errata

[0] You could also use function decorators to handle a lot of the logic. That creates more code to maintain and CBVs do help alleviate redundant code. This is explained later.

[1] Python makes it somewhat difficult compared to Java. There is no distinction being made between an interface, Java's term for a mixin, and an object. Python assumes you should know what you're doing. If you want a class that doesn't actually get used on its own but only as a mixin, then make sure to use it as such. The compiler/interpreter will not help you differentiate.

[2] This assumes the default primary key is used for the object. Django autopopulates one as pk (e.g. quip.pk) unless you specify otherwise.

[3] I find FormView never gets used in practice, you almost always end up subclassing one of the three mentioned above. We'll look over those as examples.

Share on...