Class-Based Views vs Function-Based Views in 2022

django python

The CBV vs FBV debate never dies in Django circles. Years into Django development, here’s my updated perspective on when to use each.

The Basics

Function-Based View

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from .models import Article
from .forms import ArticleForm

@login_required
def article_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    return render(request, 'articles/detail.html', {'article': article})

@login_required
def article_create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
            return redirect('article_detail', pk=article.pk)
    else:
        form = ArticleForm()
    return render(request, 'articles/form.html', {'form': form})

Class-Based View

from django.views.generic import DetailView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from .models import Article
from .forms import ArticleForm

class ArticleDetailView(LoginRequiredMixin, DetailView):
    model = Article
    template_name = 'articles/detail.html'
    context_object_name = 'article'

class ArticleCreateView(LoginRequiredMixin, CreateView):
    model = Article
    form_class = ArticleForm
    template_name = 'articles/form.html'
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
    
    def get_success_url(self):
        return reverse_lazy('article_detail', kwargs={'pk': self.object.pk})

When to Use FBVs

Simple, Unique Logic

@login_required
def dashboard(request):
    """Complex dashboard with multiple data sources."""
    user = request.user
    
    context = {
        'recent_articles': Article.objects.filter(author=user)[:5],
        'pending_reviews': Review.objects.filter(reviewer=user, status='pending'),
        'notifications': Notification.objects.filter(user=user, read=False),
        'stats': calculate_user_stats(user),
    }
    
    return render(request, 'dashboard.html', context)

This doesn’t fit any generic pattern. FBV is clearer.

API Endpoints

from django.http import JsonResponse
from django.views.decorators.http import require_GET

@require_GET
def api_search(request):
    query = request.GET.get('q', '')
    if len(query) < 3:
        return JsonResponse({'error': 'Query too short'}, status=400)
    
    results = Article.objects.filter(title__icontains=query)[:10]
    return JsonResponse({
        'results': [
            {'id': a.id, 'title': a.title}
            for a in results
        ]
    })

For simple API responses, FBVs are direct.

When You Need Explicit Control Flow

def checkout(request):
    """Multi-step checkout with complex flow."""
    cart = get_cart(request)
    
    if not cart.items.exists():
        messages.warning(request, "Your cart is empty")
        return redirect('cart')
    
    if request.method == 'POST':
        form = CheckoutForm(request.POST)
        if form.is_valid():
            # Payment processing
            payment_result = process_payment(form.cleaned_data, cart)
            
            if payment_result.success:
                order = create_order(cart, form.cleaned_data)
                clear_cart(request)
                send_confirmation_email(order)
                return redirect('order_confirmation', order_id=order.id)
            else:
                messages.error(request, payment_result.error_message)
    else:
        form = CheckoutForm(initial=get_user_defaults(request.user))
    
    return render(request, 'checkout.html', {
        'form': form,
        'cart': cart,
    })

The control flow is explicit and readable.

When to Use CBVs

CRUD Operations

class ArticleListView(ListView):
    model = Article
    paginate_by = 20
    ordering = ['-created_at']

class ArticleDetailView(DetailView):
    model = Article

class ArticleCreateView(LoginRequiredMixin, CreateView):
    model = Article
    fields = ['title', 'content']

class ArticleUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Article
    fields = ['title', 'content']
    
    def test_func(self):
        return self.get_object().author == self.request.user

class ArticleDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Article
    success_url = reverse_lazy('article_list')
    
    def test_func(self):
        return self.get_object().author == self.request.user

Standard CRUD? CBVs handle boilerplate.

Shared Behavior via Mixins

class TenantMixin:
    """Filter queryset by current tenant."""
    def get_queryset(self):
        return super().get_queryset().filter(tenant=self.request.tenant)

class AuditMixin:
    """Log all modifications."""
    def form_valid(self, form):
        response = super().form_valid(form)
        log_action(self.request.user, self.object, 'modified')
        return response

class ArticleUpdateView(LoginRequiredMixin, TenantMixin, AuditMixin, UpdateView):
    model = Article
    fields = ['title', 'content']

Mixins compose behavior elegantly.

Reusable Patterns

class FilteredListView(ListView):
    """Base for filterable lists."""
    filter_class = None
    
    def get_queryset(self):
        qs = super().get_queryset()
        if self.filter_class:
            self.filter = self.filter_class(self.request.GET, queryset=qs)
            return self.filter.qs
        return qs
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['filter'] = getattr(self, 'filter', None)
        return context

# Usage
class ArticleListView(FilteredListView):
    model = Article
    filter_class = ArticleFilter
    paginate_by = 20

Define once, reuse everywhere.

The 2022 Reality

DRF Changed the Conversation

With Django REST Framework, ViewSets are standard:

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

For APIs, this is the way.

Async Views Work with Both

# Async FBV
async def async_dashboard(request):
    articles = await sync_to_async(list)(Article.objects.filter(author=request.user)[:5])
    return render(request, 'dashboard.html', {'articles': articles})

# Async CBV
class AsyncArticleView(View):
    async def get(self, request, pk):
        article = await sync_to_async(get_object_or_404)(Article, pk=pk)
        return render(request, 'article.html', {'article': article})

Both work. Choose based on other factors.

My Rules

SituationChoiceReason
CRUD for a modelCBVBuilt-in behavior
Unique complex logicFBVExplicit control
Shared behavior neededCBV + MixinsComposition
Quick API endpointFBVSimplicity
REST APIDRF ViewSetStandard for APIs
Learning DjangoFBV firstUnderstand the basics

Final Thoughts

The debate is less heated than it was. Both have their place:

Use both. Let the use case decide.


The best view is the one that’s easy to understand.

All posts