Django - Pagination Tutorial (Python)

How to split lists into pages.

Updated May 12, 2024

Making Money with Django and AI: How to Build SaaS Services Using Python

Learn how to build AI-driven websites with the Django web framework and monetize your services.
Read More
You will receive the book in PDF and ePub formats.

Table of contents

You will learn

  • How to paginate content:

Class-based view

Edit blog/views.py and add these lines:

from django.views.generic import DetailView
from django.views.generic import ListView # here
from blog.models import Post

# here:
class BlogView(ListView): 
    model = Post
    paginate_by = 5
    template_name = 'blog/index.html'
    context_object_name = 'posts'

class PostDetailView(DetailView):
    model = Post
  • ListView is a convenience class for displaying a list of objects.
  • paginate_by = 5 indicates that the blog posts will be paginated with 5 posts per page.
  • context_object_name = 'posts' specifies the name under which the list of objects will be available in the template.

Blog URLs

Edit blog/urls.py and add these lines:

from django.urls import path
from blog.views import PostDetailView, BlogView # import BlogView

app_name = 'blog'

urlpatterns = [
    path('', BlogView.as_view(), name='blog'), # here
    path('<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
]

Blog index template

Create templates/blog/index.html and add these lines to it:

<ul>
    {% for post in posts %}
    <li>
        <a href="{{ post.get_absolute_url}}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>

{% if page_obj.paginator.num_pages > 1 %}
    {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">previous</a>
    {% endif %}
    <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">next</a>
    {% endif %}
{% endif %}

Create blog posts

Run this command:

python manage.py shell

Copy paste these lines to the interactive prompt:

from blog.models import Post
for i in range(15):
    content = "Lorem ipsum %s" % i
    slug = "lorem-ipsum-%s" % i
    Post.objects.create(title=content, body=content, slug=slug)

Visit /blog/ and you should see the paginator working:

Function-based approach

For function-based views, you can use the Paginator class. Edit blog/views.py and add these lines:

from django.shortcuts import render # here
from django.core.paginator import Paginator # here
from django.views.generic import DetailView
from django.views.generic import ListView
from blog.models import Post

class BlogView(ListView):
    model = Post
    paginate_by = 5
    template_name = 'blog/index.html'
    context_object_name = 'posts'

class PostDetailView(DetailView):
    model = Post

# here
def index2(request):
    posts = Post.objects.all()
    paginator = Paginator(posts, 5)
    page = request.GET.get('page')
    posts = paginator.get_page(page)
    
    return render(request,
                  'blog/index2.html',
                  {'posts': posts})

Edit blog/urls.py and add these lines:

from django.urls import path
from blog.views import PostDetailView, BlogView
from blog.views import index2 # here

app_name = 'blog'

urlpatterns = [
    path('', BlogView.as_view(), name='blog'),
    path('index2/', index2, name='index2'), # here
    path('<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
]

Create templates/blog/index2.html and add these lines to it:

<ul>
    {% for post in posts %}
    <li>
        <a href="{{ post.get_absolute_url}}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>

{% if posts.paginator.num_pages > 1 %}
    {% if posts.has_previous %}
        <a href="?page={{ posts.previous_page_number }}">previous</a>
    {% endif %}
    <span>Page {{ posts.number }} of {{ posts.paginator.num_pages }}</span>
    {% if posts.has_next %}
        <a href="?page={{ posts.next_page_number }}">next</a>
    {% endif %}
{% endif %}

Note: in this function-based example, we use the posts object to access the pagination data, not page_obj. Visit /blog/index2/ and you should see the same result:

Blue theme with clickable page numbers

Here is an example how to create and style a paginator that shows page numbers that you can click. Create templates/blue_theme.html and add these lines to it:

<!doctype html>
{% load static %}
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
        integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
    <style>
        .pagination {
            margin-top: 1em;
        }
        
        .pagination a {
            text-decoration: none;
        }
        
        .pagination-number {
            padding: 0.5em 0.8em;
            border-radius: 2px;
            color: #fff;
            background-color: #6D85C7;
        }
        
        .pagination-number:hover,
        .pagination-current {
            background-color: #3354AA;
        }
        
        .pagination-action {
            margin: 0 0.1em;
            display: inline-block;
            padding: 0.5em 0.5em;
            color: #B9B9B9;
            font-size: 1.3em;
        }
        
        .pagination-action:hover,
        .pagination-previous,
        .pagination-next {
            color: #3354AA;
        }
    </style>
    <title>Blue Pagination Theme</title>
</head>

<body>

    <ul>
        {% for post in posts %}
        <li>
            <a href="{{ post.get_absolute_url}}">{{ post.title }}</a>
        </li>
        {% endfor %}
    </ul>
    
    <div class="pagination">
        {% if page_obj.has_previous %}
        <a class="pagination-action" href="?page=1">
            <i class="fa fa-angle-double-left" aria-hidden="true"></i> </a>
        <a class="pagination-action" href="?page={{ page_obj.previous_page_number }}">
            <i class="fa fa-angle-left" aria-hidden="true"></i>
        </a>
        {% endif %}
        {% for num in page_obj.paginator.page_range %}
            {% if page_obj.number == num %}
                <span class="pagination-number pagination-current">{{ num }}</span>
            {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
                <a class="pagination-number" href="?page={{ num }}">{{ num }}</a>
            {% endif %}
        {% endfor %}
        {% if page_obj.has_next %}
            <a class="pagination-action" href="?page={{ page_obj.next_page_number }}">
                <i class="fa fa-angle-right" aria-hidden="true"></i>
            </a>
            <a class="pagination-action" href="?page={{ page_obj.paginator.num_pages }}">
                <i class="fa fa-angle-double-right" aria-hidden="true"></i>
            </a>
        {% endif %}
    </div>

</body>

</html>

Edit blog/views.py and use the new template instead:

from django.shortcuts import render
from django.core.paginator import Paginator
from django.views.generic import DetailView
from django.views.generic import ListView
from blog.models import Post

class BlogView(ListView):
    model = Post
    paginate_by = 5
    # template_name = 'blog/index.html'
    template_name = 'blog/blue_theme.html' # here
    context_object_name = 'posts'

class PostDetailView(DetailView):
    model = Post

def index2(request):
    posts = Post.objects.all()
    paginator = Paginator(posts, 5)
    page = request.GET.get('page')
    posts = paginator.get_page(page)
    
    return render(request,
                  'blog/index2.html',
                  {'posts': posts})

Visit /blog/ and you should see this:

Leave a comment

You can use Markdown to format your comment.