Using django conventions to reduce boilerplate / Django /

Introduction

Developers are introduced to Django as "The web framework for perfectionists with deadlines"; This is not just a catch phrase. Django offers features that help developers write clean, maintainable and efficient code. It achieves this, among other things, by following a set of conventions that are often overlooked.

In this post, we'll review some of these conventions and how you can use them to reduce boilerplate and remain consistent across projects.

Legend: CBV = Class Based Views / GCBV = Generic Class Based Views

GCBV - Template Name

Instead of this:

class PostListView(ListView):
    model = Post
    template_name = 'blog/posts_template.html'

You can write this:

class PostListView(ListView):
    model = Post

When using GCBV, you can omit the template_name attribute by naming your templates accordingly. For a model named Post in an app named blog, the template names should be as follows:

View Template
ListView blog/post_list.html
DetailView blog/post_detail.html
CreateView blog/post_form.html
UpdateView blog/post_form.html
DeleteView blog/post_confirm_delete.html

Django will infer the template name based on the model and app name. Developers reading your code will be able to discern what template will be used as well as infer the model from the template names.

GCBV - Context Object Name

Instead of this:

class PostListView(ListView):
    model = Post
    context_object_name = 'posts'

You can write this:

class PostListView(ListView):
    model = Post

When using GCBV, you can omit the context_object_name attribute altogether. Django will automatically set the context object name based on the model name. For a model named Post, the context object names are as follows:

View Context Object Name Or
ListView post_list object_list
DetailView post object
CreateView post object
UpdateView post object
DeleteView post object

Django will infer the context object name based on the model, so you don't need to specify it explicitly. I myself prefer to use the object version instead of the "explicit" version so that the template can be used with another view without having to make any changes.

GCBV - PK & Slug URL Parameters

Instead of this:

class PostDetailView(DetailView):
    model = Post
    pk_url_kwarg = 'post_id'
    slug_url_kwarg = 'post_slug'

# urls.py
path('post/<int:post_id>/<slug:post_slug>/', PostDetailView.as_view(), name='post_detail')

You can write this:

class PostDetailView(DetailView):
    model = Post

# urls.py
path('post/<int:pk>/<slug:slug>/', PostDetailView.as_view(), name='post_detail')

When using GCBV, you can omit the pk_url_kwarg and slug_url_kwarg attributes altogether as long as you follow the convention of naming your primary key kwarg as pk and your slug kwarg as slug.

CBV - Extra Context

Instead of writing this:

class PostDetailView(DetailView):
    model = Post

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = 'Post Detail'
        return context

You can write this:

class PostDetailView(DetailView):
    model = Post
    extra_context = {'title': 'Post Detail'}

When using Class Based Views, you can use the extra_context attribute to add static context instead of overriding the get_context_data method. This will only work for static context and does not support callables.

GCBV - Date Based Views

While not a convention, it's worth mentioning that Django provides a set of date-based views that can be used to display objects based on their date fields. These views are:

These views are particularly useful when you want to display objects based on their date fields. For example, if you have a Post model with a created_at field, you can use the DayArchiveView to display all posts created on a particular day. For example:

# views.py
class PostDayArchiveView(DayArchiveView):
    model = Post
    date_field = 'created_at'

# urls.py
path(
    'post/<int:year>/<int:month>/<int:day>/',
    DayArchiveView.as_view(),
    name='post_archive_day'
)

Notice how slim the view is compared to doing this manually? This is the power of Django's generic class-based views. Year, month and day are automatically passed and used by the view because they were "named" correctly.

Models - Related Name

Instead of this:

class Post(Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    ...

You can write this:

class Post(Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    ...

When defining a ForeignKey field, you do not have to specify the related_name attribute. Django will automatically set the related name based on the model name. For a model named Post, the related name will be post_set.

This point is quite controversial. You'll find that an overwhelming majority will argue against this and say that you should always specify the related_name attribute because it's more pythonic and readable to have user.posts versus user.post_set. I'd go as far as saying that for the very same readability reason, an indicator such as _set will help the readers distinguish a related from a none-related attribute[1]. This will also completely [2] eliminate misnomers like:

class Post(Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author')
    # or
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='post')

Which most of the time end up staying in repositories having been overlooked by code reviewers.

-

[1] Arguably, "Types" may improve readability, but that's a whole other topic. How many devs/projects actually use them?

[2] This is not always the case, given you might have numerous foreign keys to the same model, in which case you'd have to specify the related_name attribute. I'd still argue in this particular case, you're better off explicitely specifying the _set suffix to all related names. Try to be consistent in your decision.