Пагинация списка товаров в Django: инструкция для новичка
За полчаса добавите постраничный вывод к каталогу на Django 5.x: класс Paginator, ссылки «вперёд-назад» и правильную работу с фильтрами в URL.

К концу этой инструкции ваш каталог товаров перестанет вываливать все 800 позиций на одну страницу. Появится разбивка по 20 штук с кнопками навигации внизу, а нагрузка на базу и браузер упадёт. Займёт это около получаса, если проект на Django уже запускается и открывается в браузере.
Что нужно до начала
Инструкция написана для Django 5.x (проверьте командой python -m django --version), но класс Paginator работает так же начиная с Django 2.x — принципиальных отличий в API нет.
- Запущенный проект Django с приложением, где есть модель товара — назовём её
Product. - Работающая вьюха, которая уже выводит список товаров (пусть и весь сразу).
- Шаблон этого списка, куда вы можете дописать пару строк HTML.
- Доступ к редактированию файлов
views.pyи шаблона — то есть вы правите код локально или на сервере, где есть права на запись.
Если вьюха у вас на классах (ListView), путь будет короче — об этом в конце. Начнём с ручного варианта на Paginator: он показывает, как всё устроено, и работает в любой вьюхе.
Шаг 1. Импортируйте Paginator
Откройте views.py вашего приложения и добавьте импорт вверху файла.
from django.core.paginator import Paginator
Класс Paginator живёт в django.core.paginator — это часть ядра фреймворка, ставить ничего не нужно. Проверка: сохраните файл и перезапустите сервер разработки. Если он поднялся без ImportError — путь указан верно.
Шаг 2. Разбейте queryset на страницы во вьюхе
Найдите функцию, которая отдаёт список товаров. Она выглядит примерно так:
from django.shortcuts import render
from .models import Product
def product_list(request):
products = Product.objects.all().order_by("name")
return render(request, "catalog/product_list.html", {"products": products})
Обратите внимание на order_by: без явной сортировки Django выдаёт предупреждение UnorderedObjectListWarning, а товары могут прыгать между страницами при обновлении. Сортировка по любому стабильному полю обязательна.
Теперь оберните queryset в Paginator и достаньте нужную страницу.
from django.shortcuts import render
from django.core.paginator import Paginator
from .models import Product
def product_list(request):
products = Product.objects.all().order_by("name")
# 20 товаров на страницу
paginator = Paginator(products, 20)
# номер страницы приходит из ?page=2 в адресе, по умолчанию 1
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(request, "catalog/product_list.html", {"page_obj": page_obj})
Метод get_page() удобнее, чем page(): если в адресе окажется ?page=abc или ?page=999, он не упадёт с ошибкой, а вернёт первую или последнюю страницу. Это то, что нужно новичку.
Проверка: page_obj — это не список товаров, а объект страницы. Товары внутри него, перебираются циклом. Дальше поправим шаблон.
Шаг 3. Выведите товары и навигацию в шаблоне
В шаблоне вы, скорее всего, перебирали products. Теперь перебирайте page_obj — по нему тоже можно пройтись циклом, он отдаёт только товары текущей страницы.
<ul>
{% for product in page_obj %}
<li>{{ product.name }} — {{ product.price }} руб.</li>
{% endfor %}
</ul>
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Назад</a>
{% endif %}
<span>
Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Вперёд</a>
{% endif %}
</div>
Разберём, что откуда:
page_obj.has_previous/has_next— есть ли соседние страницы, чтобы не рисовать ссылку в никуда.page_obj.previous_page_number/next_page_number— номера для ссылок.page_obj.number— текущая страница.page_obj.paginator.num_pages— всего страниц.
Проверка: откройте каталог в браузере. Внизу появятся кнопки и строка «Страница 1 из N». Нажмите «Вперёд» — адрес станет ?page=2, товары сменятся.
Пагинация без сортировки — это баг, который проявится не сразу. Пока товаров мало, всё выглядит нормально. Когда их станет пара сотен, одни и те же позиции начнут появляться на разных страницах, а другие исчезнут. Всегда ставьте order_by.
Шаг 4. Не потеряйте фильтры и поиск в ссылках
Если в каталоге есть фильтр вроде ?category=shoes, то ссылка ?page=2 затрёт его — параметр category исчезнет из адреса, и фильтр слетит при переходе на вторую страницу. Это самая частая ошибка новичков с пагинацией.
Решение — собирать остальные GET-параметры вручную. Простой способ в шаблоне:
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&category={{ request.GET.category }}">Вперёд</a>
{% endif %}
Но так придётся дописывать каждый параметр руками. Чище — передать во вьюхе строку с параметрами без page:
def product_list(request):
products = Product.objects.all().order_by("name")
paginator = Paginator(products, 20)
page_obj = paginator.get_page(request.GET.get("page"))
# копируем GET и убираем page, чтобы не задвоился
query = request.GET.copy()
query.pop("page", None)
querystring = query.urlencode()
return render(request, "catalog/product_list.html", {
"page_obj": page_obj,
"querystring": querystring,
})
И в шаблоне:
<a href="?page={{ page_obj.next_page_number }}&{{ querystring }}">Вперёд</a>
Проверка: примените фильтр, перейдите на вторую страницу — в адресе должны остаться и page=2, и параметр фильтра.
Если не получилось
- Товары не выводятся вообще, а ошибки нет. Скорее всего, в шаблоне остался старый цикл
{% for product in products %}, а в контекст вы передаётеpage_obj. Имена должны совпадать. - UnorderedObjectListWarning в консоли. У queryset нет сортировки. Добавьте
.order_by("name")или задайтеorderingвMetaмодели. - EmptyPage при ручном методе page(). Вы использовали
paginator.page(n)вместоget_page(n). Замените наget_page— он не падает на несуществующих страницах. - Ссылка «Вперёд» ведёт на страницу без товаров. Такое бывает, если общее число товаров кратно размеру страницы плюс кэш. Проверьте
num_pages— навигация должна опираться наhas_next, а не на жёстко заданное число. - Фильтр слетает на второй странице. Не передан
querystringв ссылку — вернитесь к шагу 4.
Если у вас ListView
Для вьюхи на классе ListView пагинация включается одной строкой — не нужно вручную создавать Paginator:
from django.views.generic import ListView
from .models import Product
class ProductListView(ListView):
model = Product
ordering = ["name"]
paginate_by = 20
template_name = "catalog/product_list.html"
Django сам создаст page_obj и is_paginated в контексте шаблона. Шаблон из шага 3 подойдёт без изменений. Строку с фильтрами (querystring) для ListView добавляют через переопределение get_context_data.
Что дальше
Пагинация работает — теперь стоит настроить пару вещей вокруг неё. Вынесите блок навигации в отдельный файл шаблона и подключайте через {% include %}, чтобы не копировать его в каждый список. Если товаров десятки тысяч, посмотрите в сторону count()-нагрузки: num_pages дёргает COUNT(*) на каждой странице, и на больших таблицах это отдельная тема для оптимизации. А если каталог грузится через JavaScript без перезагрузки страницы, изучите Django REST Framework и его классы пагинации — там своя логика.
Дефектный акт по фото: Осмотр по фотографиям: классификация дефектов, ссылки на нормы, требования к устранению. Готовый акт в Word.
Спросить за 80 ₽Источники: Django documentation — Pagination, Django documentation — ListView (pagination)
Частые вопросы
Работает ли Paginator в моей старой версии Django?
Как поменять число товаров на странице?
Почему товары дублируются на разных страницах?
Что будет, если пользователь введёт ?page=999 вручную?
Пагинация не сработала на реальном сайте, хотя локально всё было хорошо. В чём дело?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.