Implementing Pagination in Django: Basic and Advanced Approaches

Model Definition

Create a model to represent the data you want to pagiante:

class Article(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    author = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = 'articles'

URL Configuration

path('articles/', views.article_list, name='article_list'),

Basic Approach: Manual Pagination in Views

from django.shortcuts import render
from .models import Article
from django.utils.safestring import mark_safe

def article_list(request):
    current_page = int(request.GET.get('page', 1))
    items_per_page = 10
    offset = (current_page - 1) * items_per_page
    limit = current_page * items_per_page

    total_items = Article.objects.count()
    total_pages, remainder = divmod(total_items, items_per_page)
    if remainder:
        total_pages += 1

    dataset = Article.objects.order_by('-id')[offset:limit]

    page_window = 5
    if total_pages <= page_window * 2 + 1:
        page_start = 1
        page_end = total_pages
    else:
        if current_page <= page_window:
            page_start = 1
            page_end = page_window * 2
        else:
            if (current_page + page_window) > total_pages:
                page_start = total_pages - page_window * 2
                page_end = total_pages
            else:
                page_start = current_page - page_window
                page_end = current_page + page_window

    pagination_tags = []

    pagination_tags.append(
        '<li><a href="?page=1">First</a></li>'
    )

    if current_page > 1:
        prev_tag = '<li><a href="?page={}" aria-label="Previous">‹</a></li>'.format(
            current_page - 1
        )
    else:
        prev_tag = '<li><a href="?page=1" aria-label="Previous">‹</a></li>'
    pagination_tags.append(prev_tag)

    for num in range(page_start, page_end + 1):
        if num == current_page:
            tag = '<li class="active"><a href="?page={}">{}</a></li>'.format(num, num)
        else:
            tag = '<li><a href="?page={}">{}</a></li>'.format(num, num)
        pagination_tags.append(tag)

    if current_page < total_pages:
        next_tag = '<li><a href="?page={}" aria-label="Next">›</a></li>'.format(
            current_page + 1
        )
    else:
        next_tag = '<li><a href="?page={}" aria-label="Next">›</a></li>'.format(total_pages)
    pagination_tags.append(next_tag)

    pagination_tags.append(
        '<li><a href="?page={}">Last</a></li>'.format(total_pages)
    )

    jump_form = '''
    <li>
        <form method="get" style="display: inline-block;">
            <div class="input-group" style="width: 150px;">
                <input name="page" type="text" class="form-control" placeholder="Go to">
                <span class="input-group-btn">
                    <button class="btn btn-default" type="submit">Go</button>
                </span>
            </div>
        </form>
    </li>
    '''
    pagination_tags.append(jump_form)

    page_html = mark_safe("".join(pagination_tags))
    return render(request, 'articles/list.html', {
        'articles': dataset,
        'page_html': page_html
    })

Template Implementation

<div class="article-container">
    {% for article in articles %}
    <div class="article-item">
        <h3>{{ article.title }}</h3>
        <p>{{ article.content|truncatewords:30 }}</p>
        <span class="author">{{ article.author }}</span>
    </div>
    {% endfor %}

    <nav aria-label="Page navigation">
        <ul class="pagination">
            {{ page_html }}
        </ul>
    </nav>
</div>

Advanced Approach: Reusable Pagination Component

Create a utils directory within your app and add paginate.py:

from django.utils.safestring import mark_safe

class PageNator:
    def __init__(self, request, queryset, page_size=10, window=5, param='page'):
        self.request = request
        self.queryset = queryset
        self.page_size = page_size
        self.window = window
        self.param = param

        page_num = request.GET.get(param, '1')
        if page_num.isdigit():
            self.page_num = int(page_num)
        else:
            self.page_num = 1

        self.start_index = (self.page_num - 1) * page_size
        self.end_index = self.page_num * page_size
        self.paginated_queryset = queryset[self.start_index:self.end_index]

        total_count = queryset.count()
        pages, mod = divmod(total_count, page_size)
        if mod:
            pages += 1
        self.total_pages = pages

    @property
    def count(self):
        return self.total_pages

    def generate_range(self):
        if self.total_pages <= self.window * 2 + 1:
            return range(1, self.total_pages + 1)
        elif self.page_num <= self.window:
            return range(1, self.window * 2 + 1)
        elif self.page_num + self.window >= self.total_pages:
            return range(self.total_pages - self.window * 2, self.total_pages + 1)
        else:
            return range(self.page_num - self.window, self.page_num + self.window + 1)

    def render(self):
        page_range = self.generate_range()
        html_parts = []

        html_parts.append(
            '<li><a href="?{}={}">First</a></li>'.format(self.param, 1)
        )

        if self.page_num > 1:
            html_parts.append(
                '<li><a href="?{}={}" aria-label="Previous">‹</a></li>'.format(
                    self.param, self.page_num - 1
                )
            )
        else:
            html_parts.append(
                '<li><a href="?{}={}" aria-label="Previous">‹</a></li>'.format(self.param, 1)
            )

        for num in page_range:
            if num == self.page_num:
                html_parts.append(
                    '<li class="active"><a href="?{}={}">{}</a></li>'.format(
                        self.param, num, num
                    )
                )
            else:
                html_parts.append(
                    '<li><a href="?{}={}">{}</a></li>'.format(self.param, num, num)
                )

        if self.page_num < self.total_pages:
            html_parts.append(
                '<li><a href="?{}={}" aria-label="Next">›</a></li>'.format(
                    self.param, self.page_num + 1
                )
            )
        else:
            html_parts.append(
                '<li><a href="?{}={}" aria-label="Next">›</a></li>'.format(
                    self.param, self.total_pages
                )
            )

        html_parts.append(
            '<li><a href="?{}={}">Last</a></li>'.format(self.param, self.total_pages)
        )

        jump_input = '''
        <li>
            <form method="get">
                <div class="input-group">
                    <input name="{}" type="text" class="form-control" placeholder="Page #">
                    <span class="input-group-btn">
                        <button class="btn btn-default" type="submit">Go</button>
                    </span>
                </div>
            </form>
        </li>
        '''.format(self.param)
        html_parts.append(jump_input)

        return mark_safe("".join(html_parts))

Usage in views:

from .utils.paginate import PageNator

def article_list(request):
    queryset = Article.objects.all()
    paginator = PageNator(request, queryset)
    
    return render(request, 'articles/list.html', {
        'articles': paginator.paginated_queryset,
        'page_html': paginator.render()
    })

Enhanced Vertion: Preserve Query Parameters

This version maintains search filters and other query parameters when navigating between pages:

from django.utils.safestring import mark_safe
from django.http.request import QueryDict
import copy

class PageNator:
    def __init__(self, request, queryset, page_size=10, window=5, param='page'):
        self.request = request
        self.queryset = queryset
        self.page_size = page_size
        self.window = window
        self.param = param

        query_params = copy.deepcopy(request.GET)
        query_params._mutable = True
        self.query_params = query_params

        page_num = request.GET.get(param, '1')
        if page_num.isdigit():
            self.page_num = int(page_num)
        else:
            self.page_num = 1

        self.start_index = (self.page_num - 1) * page_size
        self.end_index = self.page_num * page_size
        self.paginated_queryset = queryset[self.start_index:self.end_index]

        total_count = queryset.count()
        pages, mod = divmod(total_count, page_size)
        if mod:
            pages += 1
        self.total_pages = pages

    def generate_range(self):
        if self.total_pages <= self.window * 2 + 1:
            return range(1, self.total_pages + 1)
        elif self.page_num <= self.window:
            return range(1, self.window * 2 + 1)
        elif self.page_num + self.window >= self.total_pages:
            return range(self.total_pages - self.window * 2, self.total_pages + 1)
        else:
            return range(self.page_num - self.window, self.page_num + self.window + 1)

    def render(self):
        page_range = self.generate_range()
        html_parts = []

        self.query_params.setlist(self.param, [1])
        html_parts.append(
            '<li><a href="?{}">First</a></li>'.format(self.query_params.urlencode())
        )

        if self.page_num > 1:
            self.query_params.setlist(self.param, [self.page_num - 1])
            html_parts.append(
                '<li><a href="?{}" aria-label="Previous">‹</a></li>'.format(
                    self.query_params.urlencode()
                )
            )
        else:
            self.query_params.setlist(self.param, [1])
            html_parts.append(
                '<li><a href="?{}" aria-label="Previous">‹</a></li>'.format(
                    self.query_params.urlencode()
                )
            )

        for num in page_range:
            self.query_params.setlist(self.param, [num])
            if num == self.page_num:
                html_parts.append(
                    '<li class="active"><a href="?{}">{}</a></li>'.format(
                        self.query_params.urlencode(), num
                    )
                )
            else:
                html_parts.append(
                    '<li><a href="?{}">{}</a></li>'.format(
                        self.query_params.urlencode(), num
                    )
                )

        if self.page_num < self.total_pages:
            self.query_params.setlist(self.param, [self.page_num + 1])
            html_parts.append(
                '<li><a href="?{}" aria-label="Next">›</a></li>'.format(
                    self.query_params.urlencode()
                )
            )
        else:
            self.query_params.setlist(self.param, [self.total_pages])
            html_parts.append(
                '<li><a href="?{}" aria-label="Next">›</a></li>'.format(
                    self.query_params.urlencode()
                )
            )

        self.query_params.setlist(self.param, [self.total_pages])
        html_parts.append(
            '<li><a href="?{}">Last</a></li>'.format(self.query_params.urlencode())
        )

        jump_input = '''
        <li>
            <form method="get">
                <div class="input-group">
                    <input name="{}" type="text" class="form-control" placeholder="Page #">
                    <span class="input-group-btn">
                        <button class="btn btn-default" type="submit">Go</button>
                    </span>
                </div>
            </form>
        </li>
        '''.format(self.param)
        html_parts.append(jump_input)

        return mark_safe("".join(html_parts))

With this implementation, URL parameters like ?search=django&page=3 are preserved across pagination links, ensuring filters remain active when users navigate through pages.

Tags: Django Pagination web development python Backend

Posted on Thu, 02 Jul 2026 16:45:13 +0000 by daredevil14