Video coming soon

The recording for this lesson has not been published yet.

What we cover

  • Where generic views save real work
  • Where they only add indirection
  • Readability for the next developer
  • Mixed codebases and team conventions

Lesson notes

1. The wrong question and the right one

After four lessons of conversions it is tempting to conclude that class-based views are the "grown up" way to write Django. They are not. Both styles receive a request and return a response, and Django treats them identically.

The useful question is not "which is better" but: does this view do something a generic view already does? If yes, subclass it. If no, a function is usually shorter and always more obvious.

New view. What does it do? list / detail / form / delete? yes no Generic view ListView, DetailView, CreateView Function view webhook, export, one-off logic More than 3 overrides and nothing standard left? Go back. Written a third time? Then extract a class.

2. Where generic views save real work

  • Plain CRUD. PostListView, PostDetailView, PostCreateView - four lines each instead of forty, plus pagination and a correct 404 for free.
  • The same pattern for many models. Twelve models with an admin-like editor is twelve four-line classes.
  • Shared policy. LoginRequiredMixin, AuthorRequiredMixin and your own mixins apply the rule in one place instead of at the top of every function.
  • Small variations of one view. A "my posts" page is PostListView with a three-line get_queryset().
class MyPostListView(LoginRequiredMixin, PostListView):
    template_name = "blog/my_posts.html"

    def get_queryset(self):
        return super().get_queryset().filter(author=self.request.user)

That is the strongest argument for the class style: subclassing an existing view of your own. You cannot do that with a function without copying it.

3. Where they only add indirection

A generic view stops helping the moment the flow is not "one queryset, one template". These are all better as functions:

  • Anything that is not a page. A webhook receiver, a CSV export, a health check.
  • Multiple forms or several models in one request. The mixins fight over form_valid() and get_context_data().
  • Branching logic. Three outcomes depending on payment state read fine as if/elif, and terribly as five overridden methods.
  • A view that is used once and never extended.
# blog/views.py - a function is the right tool here
import csv

from django.contrib.admin.views.decorators import staff_member_required
from django.http import HttpResponse


@staff_member_required
def export_posts_csv(request):
    response = HttpResponse(content_type="text/csv")
    response["Content-Disposition"] = 'attachment; filename="posts.csv"'
    writer = csv.writer(response)
    writer.writerow(["title", "author", "published"])
    for post in Post.objects.select_related("author").iterator():
        writer.writerow([post.title, post.author.username, post.published_date])
    return response
# the same thing as a class: more ceremony, no gain
class ExportPostsCsvView(UserPassesTestMixin, View):
    def test_func(self):
        return self.request.user.is_staff

    def get(self, request):
        ...  # identical body

4. The trade-offs, side by side

AspectFunction viewClass-based view
Reading it top to bottom, in one place you must know the MRO and the defaults
Reuse copy and paste, or extract helpers inheritance and mixins
Standard CRUD 30-40 lines you maintain 4-8 lines Django maintains
Unusual flow trivial fights the base class
Decorators @login_required directly mixins, or method_decorator
Testing call it, or use the test client test client; as_view() for unit tests

5. Readability for the next developer

The next developer is you in six months. Three habits keep a class-based view honest:

  • Say the attributes out loud. Write template_name and context_object_name even when the default matches. One line of explicitness saves a trip to the documentation.
  • Keep the inheritance shallow. Your class, one of your mixins, one Django generic view. Beyond that, nobody can predict the behaviour by reading.
  • Never hide business rules in a mixin nobody expects. A mixin called PageTitleMixin must not also filter the queryset.

When you do need a decorator on a class, the tool is method_decorator on dispatch():

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page


@method_decorator(cache_page(60 * 5), name="dispatch")
class PostListView(ListView):
    model = Post

6. Refactoring between the two styles safely

A conversion must not change behaviour, so let the tests decide, not your reading of the diff. Write the test against the URL first - it stays valid in both styles:

# blog/tests/test_views.py
class PostListTests(TestCase):
    def test_only_published_posts_are_listed(self):
        response = self.client.get(reverse("blog:post_list"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Published post")
        self.assertNotContains(response, "Draft post")
        self.assertTemplateUsed(response, "blog/post_list.html")
  1. Write or extend the tests for the existing view. They must pass before you touch it.
  2. Add the class next to the function - do not delete anything yet.
  3. Point the URLconf at views.PostListView.as_view(), keeping the same name so no template or redirect changes.
  4. Run the tests. Then click through the page, including page 2 and an invalid form.
  5. Delete the function in a separate commit.

The reverse direction is just as legitimate. If a class has grown five overrides, inline them into a function, keep the URL name, and delete the class.

7. Mixed codebases and team conventions

Real Django projects contain both styles, and that is fine - as long as the choice follows a written rule instead of the mood of the day. A convention that works:

blog/views.py

  1. Class-based generic views for CRUD on a model.
  2. Function views for everything that is not CRUD
     (exports, webhooks, redirects, ajax endpoints).
  3. Shared behaviour goes into blog/mixins.py, never
     copied between views.
  4. Business logic lives in models.py or services.py -
     the view only translates HTTP.
  5. Every view has at least one test that calls its URL.

Write those five lines into your CONTRIBUTING.md. Point 4 matters more than the function-versus-class debate: a thin view is easy to move between styles, a view that owns the business rules is not.

Your blog after module 3 is a good example of the mix: four generic views for the posts, one function for the CSV export, one mixins.py for the author rule.

8. Exercise

  1. List every view in your project and label it "standard CRUD" or "other". Convert only the first group, if you have not already.
  2. Take one class-based view you converted in lesson 3.3 and count the overrides. If there are more than three, rewrite it as a function and decide which version you prefer.
  3. Add the CSV export above as a function view and resist the urge to make it a class.
  4. Write a URL-level test for one view, then convert that view to the other style without touching the test.
  5. Put your five view conventions in CONTRIBUTING.md and check your existing code against them.

Further reading

After this lesson you will

  • Choose the view style that fits the task
  • Refactor between the two styles with confidence