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.
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.
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.
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:
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:
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.
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.
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:
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.
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().
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.
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
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
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
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.
Django ships ListView, DetailView, CreateView, UpdateView, DeleteView, FormView, TemplateView, RedirectView. Between them they cover the entire CRUD surface of a typical web app.
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.
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:
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.
URL Routing
FBVs wire directly into urls.py — just reference the function:
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:
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.
FBV vs CBV — When to Use
This is the real question, and the honest answer is: it depends on what the view is doing.
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.