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:

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:

Prerequisites

Before starting, you should already know:

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:

This improves:

2. Why Use Pagination?

Pagination is useful because it:

It is especially important for:

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:

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

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

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

12. Template for CBV Pagination

With ListView, the template gets:

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:

17. Improving Large Page Lists

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

Example:

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

A common page size is:

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

This makes the blog much cleaner and easier to navigate.

21. Summary

In this tutorial, you learned that:

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.