Class-based vs Function-based Views in Django

Last updated August 1st, 2022

In this article, we'll look at the differences between Django's class-based views (CBV) and function-based views (FBV). We'll compare and contrast and dive into the pros and cons of each approach (along with Django's built-in generic class-based views). By the end, you should have a good understanding of when to use one over the other.

Contents

Introduction

One of the main uses of Django (and essentially any other web framework) is to serve HTTP responses in response to HTTP requests. Django allows us to do that using so-called views. A view is just a callable that accepts a request and returns a response.

Django initially only supported function-based views (FBVs), but they were hard to extend, didn't take advantage of object-oriented programming (OOP) principles, and weren't DRY. This is why Django developers decided to add support for class-based views (CBVs). CBVs utilize OOP principles, which allow us to use inheritance, reuse code, and generally write better and cleaner code.

We need to keep in mind that CBVs are not designed to replace FBVs. Anything you can achieve with FBVs is also achievable with CBVs. They each have their own pros and cons.

Lastly, Django offers pre-made or generic CBVs which provide solutions to common problems. They have programmer-friendly names and offer solutions to problems like displaying data, editing data, and working with date-based data. They can be used on their own or inherited in custom views.

Let's look at the different view types and learn when it's appropriate to use which.

Function-based views (FBVs)

At their core, FBVs are just functions. They're easy to read and work with since you can see exactly what's happening. Due to their simplicity, they're a great fit for Django beginners. So, if you're just starting with Django, it's recommended to have some working knowledge of FBVs before diving into CBVs.

Pros and Cons

Pros

  • Explicit code flow (you have full control over what happens)
  • Simple to implement
  • Easy to understand
  • Great for unique view logic
  • Easy to integrate with decorators

Cons

  • A lot of repeated (boilerplate) code
  • Handling of HTTP methods via conditional branching
  • Don't take advantage of OOP
  • Harder to maintain

Quick Example

An example FBV looks like this:

from django.shortcuts import render, redirect
from django.views import View


def task_create_view(request):
    if request.method == 'POST':
        form = TaskForm(data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('task-list'))

    return render(request, 'todo/task_create.html', {
        'form': TaskForm(),
    })

This view takes in a request, performs some logic, and returns an HttpResponse. Just by looking at the code, we can see the first downside: conditional branching. For each HTTP method, we have to create a separate branch. This can increase code complexity and lead to spaghetti code.

The next downside of FBVs is that they do not scale well. As your codebase grows bigger and bigger, you'll notice a lot of repeated (boilerplate) code for handling models (especially with CRUD operations). Try imagining how much a view for creating articles would differ from the example above... They'd be pretty much the same.

In order to use FBVs, we have to register them inside urls.py like so:

urlpatterns = [
    path('create/', task_create_view, name='task-create'),
]

You should opt for FBVs when you're working on highly customized view logic. In other words, FBVs are a great use case for a view that doesn't share much code with other views. A few real-world examples for using FBVs would be: a statistics view, a chart view, and a password reset view.

Todo App (using FBVs)

Let's look at how a simple todo application that allows CRUD operations would be written using only FBVs.

Firstly, we'd initialize our project, define our models, create HTML templates and then start working on views.py. We'd probably end up with something like this:

# todo/views.py

from django.shortcuts import render, get_object_or_404, redirect

from .forms import TaskForm, ConfirmForm
from .models import Task


def task_list_view(request):
    return render(request, 'todo/task_list.html', {
        'tasks': Task.objects.all(),
    })


def task_create_view(request):
    if request.method == 'POST':
        form = TaskForm(data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('task-list'))

    return render(request, 'todo/task_create.html', {
        'form': TaskForm(),
    })


def task_detail_view(request, pk):
    task = get_object_or_404(Task, pk=pk)
    return render(request, 'todo/task_detail.html', {
        'task': task,
    })


def task_update_view(request, pk):
    task = get_object_or_404(Task, pk=pk)

    if request.method == 'POST':
        form = TaskForm(instance=task, data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('task-detail', args={pk: pk}))

    return render(request, 'todo/task_update.html', {
        'task': task,
        'form': TaskForm(instance=task),
    })


def task_delete_view(request, pk):
    task = get_object_or_404(Task, pk=pk)

    if request.method == 'POST':
        form = ConfirmForm(data=request.POST)
        if form.is_valid():
            task.delete()
            return HttpResponseRedirect(reverse('task-list'))

    return render(request, 'todo/task_delete.html', {
        'task': task,
        'form': ConfirmForm(),
    })

You can get the full source code on GitHub.

We ended up with simple and straightforward view logic. You can't improve this code much.

Class-based views (CBVs)

Class-based views, which were introduced in Django 1.3, provide an alternative way to implement views as Python objects instead of functions. They allow us to use OOP principles (most importantly inheritance). We can use CBVs to generalize parts of our code and extract them as superclass views.

CBVs also allow you to use Django's built-in generic class-based views and mixins, which we'll take a look at in the next section.

Pros and Cons

Pros

  • Are extensible
  • They take advantage of OOP concepts (most importantly inheritance)
  • Great for writing CRUD views
  • Cleaner and reusable code
  • Django's built-in generic CBVs
  • They're similar to Django REST framework views

Cons

  • Implicit code flow (a lot of stuff happens in the background)
  • Use many mixins, which can be confusing
  • More complex and harder to master
  • Decorators require an extra import or code override

For more, review What are the pros and cons of using class-based views in Django/Python?

Quick Example

Let's rewrite our previous FBV example as a CBV:

from django.shortcuts import render, redirect
from django.views import View


class TaskCreateView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'todo/task_create.html', {
            'form': TaskForm(),
        })

    def post(self, request, *args, **kwargs):
        form = TaskForm(data=request.POST)
        if form.is_valid():
            task = form.save()
            return redirect('task-detail', pk=task.pk)

        return self.get(request)

We can see that this example is not much different from the FBV approach. The logic is more or less the same. The main difference is code organization. Here each HTTP method is addressed with a separate method instead of conditional branching. In CBVs you can use the following methods: get, post, put, patch, delete, head, options, trace.

Another upside of this approach is that HTTP methods that are not defined automatically return a 405 Method Not Allowed response.

When using FBVs, you can use one of the allowed HTTP method decorators, like @require_http_methods, to achieve the same thing.

Because Django's URL resolver expects a callable function, we need to call as_view() when registering them in urls.py:

urlpatterns = [
    path('create/', TaskCreateView.as_view(), name='task-create'),
]

Code Flow

The code flow for CBVs is a little more complex because some stuff happens in the background. If we extend the base View class the following code steps will be executed:

  1. An HttpRequest is routed to MyView by the Django URL dispatcher.
  2. The Django URL dispatcher calls as_view() on MyView.
  3. as_view() invokes setup() and dispatch().
  4. dispatch() triggers a method for a specific HTTP method or http_method_not_allowed().
  5. An HttpResponse is returned.

Class based-views code flow

Todo App (using CBVs)

Now, let's rewrite our todo application to only use CBVs:

# todo/views.py

from django.shortcuts import render, get_object_or_404, redirect
from django.views import View

from .forms import TaskForm, ConfirmForm
from .models import Task


class TaskListView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'todo/task_list.html', {
            'tasks': Task.objects.all(),
        })


class TaskCreateView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'todo/task_create.html', {
            'form': TaskForm(),
        })

    def post(self, request, *args, **kwargs):
        form = TaskForm(data=request.POST)
        if form.is_valid():
            task = form.save()
            return redirect('task-detail', pk=task.pk)

        return self.get(request)


class TaskDetailView(View):

    def get(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)

        return render(request, 'todo/task_detail.html', {
            'task': task,
        })


class TaskUpdateView(View):

    def get(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)
        return render(request, 'todo/task_update.html', {
            'task': task,
            'form': TaskForm(instance=task),
        })

    def post(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)
        form = TaskForm(instance=task, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('task-detail', pk=task.pk)

        return self.get(request, pk)


class TaskDeleteView(View):

    def get(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)
        return render(request, 'todo/task_confirm_delete.html', {
            'task': task,
            'form': ConfirmForm(),
        })

    def post(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)
        form = ConfirmForm(data=request.POST)
        if form.is_valid():
            task.delete()
            return redirect('task-list')

        return self.get(request, pk)

Also, let's not forget to make our urls.py call as_view():

# todo/urls.py

from django.urls import path

from .views import TaskListView, TaskDetailView, TaskCreateView, TaskUpdateView, TaskDeleteView


urlpatterns = [
    path('', TaskListView.as_view(), name='task-list'),
    path('create/', TaskCreateView.as_view(), name='task-create'),
    path('<int:pk>/', TaskDetailView.as_view(), name='task-detail'),
    path('update/<int:pk>/', TaskUpdateView.as_view(), name='task-update'),
    path('delete/<int:pk>/', TaskDeleteView.as_view(), name='task-delete'),
]

You can get the full source code on GitHub.

We sacrificed a few lines of code for a bit cleaner code. We no longer use conditional branching. If we, for example, look at TaskCreateView and TaskUpdateView, we can see that they're pretty much the same. We could further improve this code by extracting the common logic into a parent class. Additionally, we could extract the view logic and use it for views for other models.

Django's Generic Class-based Views

If you were to follow all of the refactoring suggestions mentioned in the previous section, you'd eventually be left with a view that mimics some of Django's generic class-based views. Django's generic CBVs are great for solving common problems like retrieving, creating, modifying, and deleting objects as well as pagination and archive views. They speed up the development process too.

Quick Example

Let's look at an example:

from django.views.generic import CreateView


class TaskCreateView(CreateView):
    model = Task
    context_object_name = 'task'
    fields = ('name', 'description', 'is_done')
    template_name = 'todo/task_create.html'

We created a class named TaskCreateView and inherited CreateView. By doing that we gained a lot of functionality, with almost no code. Now we just need to set the following attributes:

  1. model defines what Django model the view works with.
  2. fields is used by Django to create a form (alternatively, we could provide form_class).
  3. template_name defines which template to use (defaults to /<app_name>/<model_name>_form.html).
  4. context_object_name defines the context key under which the model instance is passed to the template (defaults to object).
  5. success_url defines where the user gets redirected on success (alternatively, you can set get_absolute_url in your model).

For more information about generic CBVs, refer to the official documentation.

As you've probably guessed, there's a lot more magic happening behind the scenes with generic CBVs. They can be confusing even for experienced Django developers. Once you get the hang of them, though, you'll probably feel like a wizard.

You should use generic CBVs for views that perform common tasks (e.g., CRUD operations). If your view needs to do something that's not covered by CBVs, use mixins or a function-based view.

Django's Built-in CBV Types

At the time of writing, Django comes with a decent amount of generic CBVs, which we can split into three categories:

Generic Display Views Generic Editing Views Generic Date-based Views

Designed to display data.

Provide a foundation for editing content.

Allow in-depth displaying of date-based data.

We'll look at practical examples of how to use them here shortly.

View Mixins

Each generic view is made to solve one problem (e.g., display information, create/update something). If your view needs to do something more than that, you can create your view using mixins. Mixins are classes that offer discrete functionality and they can be combined to solve pretty much any problem.

You can also pick a generic CBV as a base and then include additional mixins.

Even Django's generic CBVs are composed of mixins. Let's look at CreateView diagram:

CreateView diagram

We can see that it leverages a number of mixins, like ContextMixin, SingleObjectMixin, and FormMixin. They have programmer-friendly names, so you should have a general idea of what each of them does based on their name.

Mixins take a lot of time to master and can oftentimes be confusing. If you're just starting with mixins I'd suggest you start by reading Using mixins with class-based views.

Todo App (using Django's generic CBVs)

Now, let's rewrite the todo app for the final time using Django's generic class-based views:

# todo/views.py

from django.views.generic import ListView, DetailView, DeleteView, UpdateView, CreateView


class TaskListView(ListView):
    model = Task
    context_object_name = 'tasks'


class TaskCreateView(CreateView):
    model = Task
    context_object_name = 'task'
    fields = ('name', 'description', 'is_done')
    template_name = 'todo/task_create.html'


class TaskDetailView(DetailView):
    model = Task
    context_object_name = 'task'


class TaskUpdateView(UpdateView):
    model = Task
    context_object_name = 'task'
    fields = ('name', 'description', 'is_done')
    template_name = 'todo/task_update.html'


class TaskDeleteView(DeleteView):
    model = Task
    context_object_name = 'task'
    success_url = '/'

You can get the full source code on GitHub.

By using Django generic CBVs, we split our code in half. Further, the code is much cleaner and easier to maintain, but it can be much harder to read and understand if you're new to generic CBVs.

Conclusion

One view type is not better than the other. It all depends on the situation and personal preferences. Sometimes FBVs are better and other times CBVs are better. Try to remember their pros and cons to make a good decision when writing a specific view. If you're still not exactly sure when to use which, you can practice a bit by converting FBVs to CBVs and vice versa. Additionally, you can use this flowchart:

CBV vs FBV Flowchart

I personally prefer FBVs for smaller projects (that cannot be solved with generic CBVs) and opt for CBVs when dealing with larger codebases.

Nik Tomazic

Nik Tomazic

Nik is a software developer from Slovenia. He's interested in object-oriented programming and web development. He likes learning new things and accepting new challenges. When he's not coding, Nik's either swimming or watching movies.

Share this tutorial

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.