Blog

DJANGO · PYTHON · BACKEND ENGINEERING

Views in Django —
FBV vs CBV.

What are Django views, how do function-based and class-based views differ, and — most importantly — when should you use each? This is the complete breakdown, with practical examples and the honest tradeoffs.

anuragdevon 23rd May 2020 8 min read Django Deep Dive
01

What Is a View?

In Django, a view is a Python callable that receives an HTTP request and returns an HTTP response. That's the entire contract. What happens in between — querying the database, rendering a template, returning JSON — is up to you.

Every URL in a Django app maps to exactly one view. The URL dispatcher (urls.py) receives an incoming request, matches the URL pattern, and hands the request object to the corresponding view. The view does its work and returns a response.

🌐
Browser
GET /posts/
🗺️
urls.py
URL dispatcher
⚙️
View
Your logic here
📄
Response
HTML / JSON

Django gives you two ways to write views: as plain Python functions (function-based views, FBVs) or as Python classes (class-based views, CBVs). Both produce the same result — an HTTP response — but they have very different tradeoffs.

02

Function-Based Views

FBVs are the simplest possible views: a function that takes a request and returns a response. Here's a view that lists all blog posts:

PYTHON — views.py
from django.shortcuts import render
from .models import Post

def post_list(request):
    posts = Post.objects.all().order_by('-created_at')
    return render(request, 'blog/post_list.html', {'posts': posts})

And a view to show a single post by its ID:

PYTHON — views.py
from django.shortcuts import render, get_object_or_404

def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'blog/post_detail.html', {'post': post})

This is readable, obvious Python. You can trace every line. The entire flow — input, processing, output — is visible in one place. That's the core advantage of FBVs: explicit and easy to follow.

// NOTE

get_object_or_404 queries the model and automatically returns an HTTP 404 response if the object doesn't exist. It's shorthand for the try/except you'd otherwise write.

03

Handling HTTP Methods

Real views often need to handle multiple HTTP methods. A form page needs to handle both GET (show the form) and POST (process the submission). In FBVs, you branch on request.method:

PYTHON — views.py
from django.shortcuts import render, redirect
from .forms import PostForm

def post_create(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm()

    return render(request, 'blog/post_form.html', {'form': form})

This pattern — if POST: process; else: show form — is the most common view in any Django project. It's straightforward. The logic lives in one place. You can read it top-to-bottom.

But notice what happens as you add more views like this: you repeat the same boilerplate constantly. Every list view queries a model and renders a template. Every detail view does a 404-safe lookup. Every create view checks POST vs GET, validates a form, saves, and redirects. This repetition is exactly the problem CBVs solve.

04

Class-Based Views

CBVs are Django's answer to DRY view logic. Instead of a function, you write a class. Django maps HTTP methods to class methods: a GET request calls get(), a POST request calls post().

PYTHON — views.py
from django.views import View
from django.shortcuts import render

class PostListView(View):
    def get(self, request):
        posts = Post.objects.all().order_by('-created_at')
        return render(request, 'blog/post_list.html', {'posts': posts})

    def post(self, request):
        # handle POST — no if/else needed
        ...

Cleaner separation: GET logic and POST logic are separate methods instead of branches of an if statement. But the real power of CBVs isn't in the base View class — it's in Django's generic views.

05

Generic CBVs

Django ships with generic class-based views that implement the most common CRUD patterns out of the box. You configure them with class attributes instead of writing logic. The boilerplate disappears.

ListView — list all objects

PYTHON — views.py
from django.views.generic import ListView

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    ordering = ['-created_at']
    paginate_by = 10

That's the entire view. Django handles the queryset, the template context, pagination, and the response. Compare to the FBV equivalent — this is dramatically less code.

DetailView — show one object

PYTHON — views.py
from django.views.generic import DetailView

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    # Automatically 404s if pk not found
    # Passes `post` to template context automatically

CreateView — handle a creation form

PYTHON — views.py
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy

class PostCreateView(CreateView):
    model = Post
    fields = ['title', 'content']
    template_name = 'blog/post_form.html'
    success_url = reverse_lazy('post_list')

    def form_valid(self, form):
        # Customise before save — set the author
        form.instance.author = self.request.user
        return super().form_valid(form)

CreateView handles GET (show form), POST valid (save + redirect), and POST invalid (re-render with errors) — all in about ten lines. And form_valid is a hook you override only when you need to customise the save step.

// GENERIC VIEW REFERENCE

Django ships ListView, DetailView, CreateView, UpdateView, DeleteView, FormView, TemplateView, RedirectView. Between them they cover the entire CRUD surface of a typical web app.

06

Mixins

Mixins are the killer feature of CBVs. They're small classes you mix in to add a specific behaviour — without rewriting it in every view. The most common: LoginRequiredMixin.

PYTHON — views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    fields = ['title', 'content']
    template_name = 'blog/post_form.html'
    success_url = reverse_lazy('post_list')
    login_url = '/login/'  # redirect here if not authenticated

One mixin, one line, and now this view redirects unauthenticated users to the login page automatically. No if not request.user.is_authenticated checks scattered through your view logic.

Other useful mixins:

LoginRequiredMixinRedirect to login if not authenticated
PermissionRequiredMixinCheck for specific Django permission
UserPassesTestMixinCustom test function (e.g. is the author?)
SuccessMessageMixinFlash a success message after save

Mixin order matters: always put mixins before the base generic view in the class definition. Django's MRO (Method Resolution Order) resolves left-to-right, so mixins need to come first to override the base class's methods.

07

URL Routing

FBVs wire directly into urls.py — just reference the function:

PYTHON — urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('posts/', views.post_list, name='post_list'),
    path('posts/<int:pk>/', views.post_detail, name='post_detail'),
    path('posts/new/', views.post_create, name='post_create'),
]

CBVs need .as_view() — a class method that returns a callable view function Django's URL dispatcher can use:

PYTHON — urls.py
from django.urls import path
from .views import PostListView, PostDetailView, PostCreateView

urlpatterns = [
    path('posts/', PostListView.as_view(), name='post_list'),
    path('posts/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
    path('posts/new/', PostCreateView.as_view(), name='post_create'),
]

The .as_view() call is mandatory for CBVs. If you forget it, Django will raise a TypeError because a class isn't directly callable as a view.

08

FBV vs CBV — When to Use

This is the real question, and the honest answer is: it depends on what the view is doing.

Use FBVs when the logic is custom or complex
If your view doesn't map cleanly to list/detail/create/update/delete — if it combines queries, calls external APIs, or has branching logic — an FBV is often cleaner. You can see every line.
♻️
Use CBVs for standard CRUD
If you're listing model objects, showing a detail page, creating, editing, or deleting — use generic CBVs. You're not writing new logic; you're configuring existing logic.
🔒
Use CBVs + Mixins for reusable cross-cutting behaviour
Auth checks, permission checks, success messages — if the same behaviour applies to many views, put it in a mixin and mix it in once. In FBVs, this is a decorator, which is also fine.
👁️
FBVs when you need the code to be obvious
CBVs involve Django's MRO and method resolution. Tracing what get() actually does requires knowing which parent class provides it. FBVs show you everything in one function — great for code reviews and onboarding.

In practice, most Django projects use both. Generic CBVs for the standard CRUD pages, FBVs for anything custom — API endpoints, search views, dashboards with complex queries. There's no law saying you must pick one.

FBV strengthsExplicit, readable, easy to debug, full control
FBV weaknessesRepetitive for standard CRUD, cross-cutting logic via decorators only
CBV strengthsDRY, mixins, generic views eliminate boilerplate
CBV weaknessesHarder to trace, MRO complexity, overkill for simple views
RememberCBVs always need .as_view() in urls.py
END

Building with Django?

I write about Django internals, Python patterns, and backend architecture. More on Dev.to and GitHub.

Written in May 2020 — part of my Django learning series.

Contact