The recording for this lesson has not been published yet.
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.
PostListView, PostDetailView,
PostCreateView - four lines each instead of forty, plus pagination and a
correct 404 for free.LoginRequiredMixin,
AuthorRequiredMixin and your own mixins apply the rule in one place instead of
at the top of every function.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.
A generic view stops helping the moment the flow is not "one queryset, one template". These are all better as functions:
form_valid() and get_context_data().if/elif, and terribly as five overridden methods.# 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
If you have overridden get(), post(),
get_context_data() and form_valid() on the same class, you are
no longer using the generic view - you are fighting it. Inherit from
View, or write a function.
| Aspect | Function view | Class-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 |
The next developer is you in six months. Three habits keep a class-based view honest:
template_name and
context_object_name even when the default matches. One line of explicitness
saves a trip to the documentation.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
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")
views.PostListView.as_view(), keeping the same
name so no template or redirect changes.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.
Templates, redirects and tests reference blog:post_list, never
post_list the function. Keep the name and a view rewrite is invisible to the
rest of the project.
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.
CONTRIBUTING.md and check your existing
code against them.