Introduction

When a page contains too many items, it becomes difficult to read and slow to navigate. Imagine showing 500 blog posts, 1,000 products, or hundreds of comments on one single page. This is not good for user experience or performance.

That is why Django provides pagination.

Pagination means splitting a large list of objects into smaller pages. Instead of showing everything at once, you show a limited number of items per page, such as:

  • 5 posts per page
  • 10 products per page
  • 20 comments per page

In this tutorial, you will learn how pagination works in Django, how to use it in both function-based views and class-based views, and how to build clean page navigation links.

What You Will Learn

By the end of this tutorial, you will understand:

  • what pagination is
  • why pagination is important
  • how to use Django’s Paginator
  • how to paginate in function-based views
  • how to paginate in class-based views
  • how to build previous/next page links
  • how to show page numbers
  • common mistakes and best practices

Prerequisites

Before starting, you should already know:

  • Django models
  • QuerySets
  • views and templates
  • class-based views basics

1. What Is Pagination?

Pagination is the process of dividing a large set of results into multiple smaller pages.

For example, instead of:

Showing 100 blog posts on one page

 

you can show:

  • page 1 → posts 1 to 10
  • page 2 → posts 11 to 20
  • page 3 → posts 21 to 30

This improves:

  • readability
  • performance
  • navigation
  • user experience

2. Why Use Pagination?

Pagination is useful because it:

  • prevents long and heavy pages
  • improves loading speed
  • makes content easier to browse
  • gives a more professional layout
  • helps organize large datasets

It is especially important for:

  • blogs
  • product lists
  • search results
  • comments
  • dashboards
  • tables of data

3. Django’s Pagination Tool

Django provides a built-in class called Paginator.

Import it like this:

 

from django.core.paginator import Paginator

 

It takes two main arguments:

  • a list or QuerySet
  • number of items per page

4. Basic Example with Paginator

views.py

from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Post

def post_list(request):
    posts = Post.objects.all()
    paginator = Paginator(posts, 5)  # 5 posts per page

    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    return render(request, 'blog/post_list.html', {'page_obj': page_obj})

Explanation

  • posts gets all posts
  • Paginator(posts, 5) splits them into pages of 5 items
  • request.GET.get('page') gets the current page number from the URL
  • get_page(page_number) returns the requested page safely
  • page_obj is sent to the template

5. The URL Format for Pagination

Pagination usually works with query parameters.

Examples:

/posts/?page=1
/posts/?page=2
/posts/?page=3

Django reads the page number from:

request.GET.get('page')

6. Displaying Paginated Items in the Template

templates/blog/post_list.html

<h1>Blog Posts</h1>

{% for post in page_obj %}
    <h2>{{ post.title }}</h2>
    <p>{{ post.content|truncatewords:20 }}</p>
    <hr>
{% empty %}
    <p>No posts found.</p>
{% endfor %}

Notice that we loop over page_obj, not over posts.

7. Previous and Next Links

Now let us add navigation links.

<div class="pagination">
    {% if page_obj.has_previous %}
        <a href="?page=1">First</a>
        <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>
        <a href="?page={{ page_obj.paginator.num_pages }}">Last</a>
    {% endif %}
</div>

Explanation

  • has_previous checks if a previous page exists
  • previous_page_number gets the previous page number
  • number is the current page
  • paginator.num_pages is the total number of pages
  • has_next checks if a next page exists
  • next_page_number gets the next page number

8. Showing Numbered Page Links

Many websites show clickable page numbers.

<div class="pagination">
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <strong>{{ num }}</strong>
        {% else %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
</div>

This shows all page numbers and highlights the current one.

9. Full Function-Based Pagination Example

views.py

from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Post

def post_list(request):
    posts = Post.objects.all().order_by('-id')
    paginator = Paginator(posts, 5)

    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    return render(request, 'blog/post_list.html', {'page_obj': page_obj})

post_list.html

<h1>Blog Posts</h1>

{% for post in page_obj %}
    <h2>{{ post.title }}</h2>
    <p>{{ post.content|truncatewords:20 }}</p>
    <hr>
{% empty %}
    <p>No posts found.</p>
{% endfor %}

<div class="pagination">
    {% if page_obj.has_previous %}
        <a href="?page=1">First</a>
        <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
    {% endif %}

    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <strong>{{ num }}</strong>
        {% else %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}

    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">Next</a>
        <a href="?page={{ page_obj.paginator.num_pages }}">Last</a>
    {% endif %}
</div>

10. How get_page() Helps

Django provides two ways:

page()

Strict and can raise errors

page_obj = paginator.page(page_number)

get_page()

Safer and beginner-friendly

page_obj = paginator.get_page(page_number)

If the user enters an invalid page number like:

?page=abc

or

?page=9999

get_page() handles it gracefully.

For beginners, get_page() is usually the better choice.

11. Paginating Class-Based Views

Pagination is even easier with class-based views.

Example with ListView

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'
    paginate_by = 5
    ordering = ['-id']

Explanation

  • paginate_by = 5 tells Django to show 5 items per page
  • Django automatically adds pagination context variables

12. Template for CBV Pagination

With ListView, the template gets:

  • page_obj
  • is_paginated
  • paginator
  • object list (posts in this example)

Example:

<h1>Blog Posts</h1>

{% for post in posts %}
    <h2>{{ post.title }}</h2>
    <p>{{ post.content|truncatewords:20 }}</p>
    <hr>
{% empty %}
    <p>No posts found.</p>
{% endfor %}

{% if is_paginated %}
    <div class="pagination">
        {% if page_obj.has_previous %}
            <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
        {% endif %}

        <span>Page {{ page_obj.number }} of {{ paginator.num_pages }}</span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">Next</a>
        {% endif %}
    </div>
{% endif %}

13. Full Class-Based Example

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'
    paginate_by = 5
    ordering = ['-id']

urls.py

from django.urls import path
from .views import PostListView

urlpatterns = [
    path('posts/', PostListView.as_view(), name='post_list'),
]

post_list.html

<h1>Blog Posts</h1>

{% for post in posts %}
    <h2>{{ post.title }}</h2>
    <p>{{ post.content|truncatewords:20 }}</p>
{% empty %}
    <p>No posts found.</p>
{% endfor %}

{% if is_paginated %}
    <div class="pagination">
        {% if page_obj.has_previous %}
            <a href="?page=1">First</a>
            <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
        {% endif %}

        {% for num in paginator.page_range %}
            {% if page_obj.number == num %}
                <strong>{{ num }}</strong>
            {% else %}
                <a href="?page={{ num }}">{{ num }}</a>
            {% endif %}
        {% endfor %}

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">Next</a>
            <a href="?page={{ paginator.num_pages }}">Last</a>
        {% endif %}
    </div>
{% endif %}

14. Pagination with Search or Filters

A common issue happens when pagination is combined with search.

Example search URL:

/posts/?q=django&page=2

When generating pagination links, you must preserve the search query.

A simple example:

<a href="?q={{ request.GET.q }}&page={{ num }}">{{ num }}</a>

This ensures the search term stays while moving between pages.

15. Example: Search + Pagination

views.py

from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Post

def search_posts(request):
    query = request.GET.get('q', '')
    posts = Post.objects.filter(title__icontains=query).order_by('-id')

    paginator = Paginator(posts, 5)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    return render(request, 'blog/search_results.html', {
        'page_obj': page_obj,
        'query': query,
    })

Template

<h1>Search Results</h1>

<form method="get">
    <input type="text" name="q" value="{{ query }}" placeholder="Search...">
    <button type="submit">Search</button>
</form>

{% for post in page_obj %}
    <h2>{{ post.title }}</h2>
{% empty %}
    <p>No results found.</p>
{% endfor %}

<div class="pagination">
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <strong>{{ num }}</strong>
        {% else %}
            <a href="?q={{ query }}&page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
</div>

16. Common Pagination Attributes

Here are useful pagination values:

  • page_obj.number → current page number
  • page_obj.has_previous → whether previous page exists
  • page_obj.has_next → whether next page exists
  • page_obj.previous_page_number → previous page number
  • page_obj.next_page_number → next page number
  • page_obj.paginator.num_pages → total page count
  • page_obj.paginator.count → total object count

17. Improving Large Page Lists

If there are many pages, showing every number may look messy.

Example:

  • 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ...

In advanced projects, developers often show only nearby pages.

For now, beginners can display all page numbers, which is simpler.

18. Common Beginner Mistakes

Looping over the wrong variable

Wrong:

{% for post in posts %}

when using FBV pagination that only sent page_obj.

Correct:

{% for post in page_obj %}

Forgetting to read page from the URL

You need:

page_number = request.GET.get('page')

Not preserving search/filter values

This breaks pagination when using search.

Using page() instead of get_page() without handling errors

This can raise exceptions.

19. Best Practices

  • paginate large lists
  • use get_page() for safer handling
  • keep page size reasonable
  • preserve search/filter query parameters
  • highlight the current page
  • do not overload the page with too many navigation links

A common page size is:

  • 5 for blogs
  • 10 to 20 for products or dashboards

20. Mini Project Example

Imagine a blog with 53 posts and you want to show 6 per page.

views.py

from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Post

def blog_posts(request):
    posts = Post.objects.all().order_by('-created_at')
    paginator = Paginator(posts, 6)

    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    return render(request, 'blog/blog_posts.html', {'page_obj': page_obj})

Result

  • page 1 → first 6 posts
  • page 2 → next 6 posts
  • page 3 → next 6 posts
  • and so on

This makes the blog much cleaner and easier to navigate.

21. Summary

In this tutorial, you learned that:

  • pagination splits large data into smaller pages
  • Django provides the Paginator class
  • function-based views use Paginator manually
  • class-based views use paginate_by
  • templates can display previous/next links and page numbers
  • pagination improves readability and performance
  • search and filter parameters should be preserved in pagination links

Pagination is one of the most useful features in real Django applications because large lists are everywhere.

22. Mini Quiz

1. Which Django class is used for pagination?

A. Pager
B. Paginator
C. PageManager
D. SplitView

2. Which query parameter is commonly used for pagination?

A. id
B. num
C. page
D. index

3. Which method is safer for beginners?

A. page()
B. get_page()
C. paginate()
D. split_page()

4. Which attribute is used in ListView for pagination?

A. page_size
B. limit
C. paginate_by
D. items_per_page

5. What does page_obj.has_next check?

A. If there are more objects in the database
B. If a next page exists
C. If the page is empty
D. If the current page is valid

23. What Comes Next?

The next ideal tutorial is:

Tutorial: Search Functionality in Django
Subject: searching records using forms, QuerySets, and filters.