Writing a separate function view for every list page and detail page means repeating the same pattern over and over: fetch a queryset, sort it, paginate it, or call get_object_or_404 and render a template. ListView and DetailView are Django’s built-in class-based views that do exactly those jobs — you only declare the model, the queryset, and the template, and the framework handles the rest. This post replaces two hand-written function views in a blog app with these two classes.
The Problem
A typical blog app has two pages that repeat the same shape: a list of posts and the detail page of a single post. Written as function views, both get verbose: fetch a queryset, filter published posts, sort them, build a Paginator by hand and pick the current page, or call get_object_or_404 by slug. Write three or four pages like this and you are copying the same pattern again and again — and every time you change the pagination or filtering logic, you have to change it in several places.
# views.py — hàm view thủ công, lặp lại cùng một mẫufrom django.core.paginator import Paginatorfrom django.shortcuts import get_object_or_404, renderfrom .models import Postdef post_list(request): posts = Post.objects.filter(published=True).order_by("-created_at") paginator = Paginator(posts, 10) page = paginator.get_page(request.GET.get("page")) return render(request, "blog/post_list.html", {"page_obj": page})def post_detail(request, slug): post = get_object_or_404(Post, slug=slug, published=True) return render(request, "blog/post_detail.html", {"post": post})
The Solution
ListView and DetailView package exactly that pattern. ListView fetches the queryset itself, paginates it, and passes page_obj, paginator, and is_paginated into the template. DetailView looks up the object by slug or primary key on its own, and returns a 404 by itself when nothing matches. You only declare what differs: which model, how to filter, which template, and how many items per page.
# views.py — class-based view, khai báo thay vì viết lại logicfrom django.views.generic import DetailView, ListViewfrom .models import Postclass PostListView(ListView): model = Post queryset = Post.objects.filter(published=True).order_by("-created_at") template_name = "blog/post_list.html" context_object_name = "posts" paginate_by = 10class PostDetailView(DetailView): model = Post queryset = Post.objects.filter(published=True) template_name = "blog/post_detail.html" context_object_name = "post"
# urls.py — nhớ gọi .as_view() khi khai báo routefrom django.urls import pathfrom .views import PostDetailView, PostListViewurlpatterns = [ path("", PostListView.as_view(), name="post-list"), path("<slug:slug>/", PostDetailView.as_view(), name="post-detail"),]
In the list template, you loop over posts and use page_obj.has_next to draw the next-page button, because paginate_by already turns pagination on. In the detail template, the post variable is exactly the object DetailView found. If you need to filter by a URL parameter — for example, listing only the posts of one category — override get_queryset() and read self.kwargs instead of declaring a fixed queryset. When a page’s logic gets more complex than a generic class can handle, go back to a function view instead of forcing everything into ListView.
Apply It to Your Project
Open your Django app and look for function views that do just three things: fetch a list, sort it, and paginate it — or fetch one object by slug and render it. Replace each one with a ListView or DetailView, re-run the app’s tests to make sure the template receives the right context variables, and only then move on to the next page. The remaining pages — forms, POST handling, logic that pulls from several data sources — can stay as function views.
Related Posts
- Django: Let Your Model Know Its Own Link with get_absolute_url() — Stop Hardcoding URLs Everywhere — let the model generate its own URL instead of hardcoding it in views and templates.
- Django: Set related_name and on_delete Correctly So Relationships Stay Clean — clean model relationships before wiring them into list and detail views.
- Django: Fix the N+1 Query Problem with select_related in 5 Minutes — stop list views from firing one extra query per row.
Frequently Asked Questions
When should I use a function view instead of ListView or DetailView?
When the page needs complex logic: handling a POST form, combining several data sources, or running custom calculations before rendering. Generic classes only stay tidy when the page follows the pattern of fetching a list or fetching one object and displaying it.
Which template does ListView use if I don’t declare template_name?
Django follows the convention <app>/<model>_list.html for ListView and <app>/<model>_detail.html for DetailView, and the default context variable is object_list and object. Declaring template_name and context_object_name explicitly saves the next person reading the code from guessing.
How do I filter the list by a parameter from the URL?
Override get_queryset() and use self.kwargs to read the parameter, for example filtering Post by category__slug=self.kwargs["slug"]. ListView’s pagination and ordering stay exactly as they were.
Which variables does pagination put in the template?
When you set paginate_by, ListView already puts page_obj, paginator, and is_paginated into the context. Use page_obj.has_next and page_obj.next_page_number to draw the “Next page” button — there is no need to build a Paginator yourself anymore.
What if the URL uses a primary key instead of a slug?
By default DetailView reads the pk parameter from the URL and looks the object up by primary key. If the URL uses a slug, it reads the slug parameter and looks up the slug field — change the field with slug_field when needed.
💬 Have you tried this in your own project? If you hit an error or know a shorter way to do it, leave a comment below — I read them all and will add the best ideas to the post.
References
- Django documentation — Generic display views: Generic display views
- Django documentation — ListView / DetailView reference: ListView / DetailView reference
