~/TechPurAI
~/tutorials/django-from-scratch/class-based-views-listview-detailview
intermediate·part 9 of 22·2 min read

Class-based views: ListView and DetailView

Updated Aug 15, 2026Python · Django

post_list from part 4 is still returning a static template with no real data. This part replaces it with ListView, a generic class-based view that already knows how to query a model, paginate it, and pass it to a template — the same pattern the standalone Django template views tutorial introduced with TemplateView, now applied to real data.

ListView

python
# blog/views.py
from django.views.generic import ListView
from .models import Post

class PostListView(ListView):
    model = Post
    template_name = "blog/post_list.html"
    context_object_name = "posts"
    queryset = Post.objects.filter(published=True)

Set model = Post and ListView already knows to run Post.objects.all(), pass the result to the template as object_list, and paginate it once paginate_by is set (part 20). queryset overrides the default query — here, filtering to published posts only, using the exact .filter() pattern from part 8. context_object_name = "posts" is what makes the template variable posts instead of the default object_list, which reads better in the template below.

The list template

django
{# blog/templates/blog/post_list.html #}
{% extends "blog/base.html" %}

{% block title %}Latest posts{% endblock %}

{% block content %}
  {% for post in posts %}
    <article>
      <h2><a href="{% url 'post_detail' post.slug %}">{{ post.title }}</a></h2>
      <p>By {{ post.author }} on {{ post.created_at|date:"F j, Y" }}</p>
    </article>
  {% empty %}
    <p>No posts yet.</p>
  {% endfor %}
{% endblock %}

{% url 'post_detail' post.slug %} builds a URL by name instead of a hardcoded path — the URL pattern it refers to doesn't exist yet (that's next), but the syntax is worth introducing here since every link in this project uses it from here on. {% empty %} inside a {% for %} renders only when the loop has nothing to iterate — cleaner than a separate {% if posts %} wrapped around the whole block.

DetailView

python
# blog/views.py
from django.views.generic import DetailView

class PostDetailView(DetailView):
    model = Post
    template_name = "blog/post_detail.html"
    context_object_name = "post"
django
{# blog/templates/blog/post_detail.html #}
{% extends "blog/base.html" %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
  <article>
    <h1>{{ post.title }}</h1>
    <p>By {{ post.author }} on {{ post.created_at|date:"F j, Y" }}</p>
    <div>{{ post.content|linebreaks }}</div>
  </article>
{% endblock %}

DetailView fetches a single object and 404s automatically if the lookup fails — the get_object_or_404 pattern from part 8, built in. By default it looks up by primary key (pk) from the URL; part 10 switches that to the slug instead, for a URL worth actually reading.

Function-based or class-based?

Both do the same job here. ListView/DetailView remove boilerplate for exactly this shape — query a model, render a template — the same tradeoff the standalone TemplateView tutorial covers. A view with real branching logic (different behavior per HTTP method, complex permission checks) often stays clearer as a function.

Next: wiring these into URLs, and switching DetailView's lookup from an opaque numeric ID to a readable slug.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous8. QuerySets: filtering, ordering, and fetching datanext →10. URLs with parameters: primary keys, slugs, and get_object_or_404