Django Pagination
Imagine your school has 5,000 students. Loading all of them on one page would be extremely slow and hard to read. Pagination is the technique of splitting a large list of records into smaller pages — showing, for example, 10 or 20 records per page, with navigation buttons to go to the next or previous page.
What is Pagination?
Pagination divides a large dataset into smaller, manageable chunks called pages. Users can navigate between pages using "Previous", "Next", or numbered page links. Django has a built-in Paginator class that handles all the splitting and page calculation logic for you.
Example: Think of a book. Instead of printing all 500 pages as one giant scroll (which would be impossible to carry), the book is split into numbered pages. You can flip to page 7, go back to page 6, or jump to page 50. Django pagination works exactly like page numbers in a book — but for your database records.
Using the Paginator in a View
Django's Paginator class takes a list (or QuerySet) and a number (how many items per page). Here is how to add pagination to a student list view:
from django.shortcuts import render
from django.core.paginator import Paginator
from .models import Student
def student_list(request):
all_students = Student.objects.all().order_by('name')
paginator = Paginator(all_students, 10) # Show 10 students per page
page_number = request.GET.get('page') # Get the current page number from URL
page_obj = paginator.get_page(page_number) # Get the records for that page
return render(request, 'student_list.html', {'page_obj': page_obj})
The URL for page 2 would look like: /students/?page=2. Django reads the page value from the URL automatically using request.GET.get('page').
Example: ThePaginatoris like a librarian who has 500 book cards. You say "Show me 20 at a time." She splits them into 25 groups of 20. When you say "Give me group 3", she hands you cards 41 to 60. ThePaginatordoes exactly this with your database records.
Displaying Paginated Records in a Template
The page_obj object contains the records for the current page. Loop through it just like a regular list:
<h2>All Students</h2>
<!-- Student list -->
<ul>
{% for student in page_obj %}
<li>{{ student.name }} — Grade: {{ student.grade }}</li>
{% endfor %}
</ul>
<!-- Pagination navigation -->
<p>
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</p>
{% if page_obj.has_previous %}
<a href="?page=1">First</a>
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
{% 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 %}
Understanding the page_obj Attributes
The page_obj returned by paginator.get_page() has many useful attributes:
page_obj.number— The current page number (e.g., 3).page_obj.paginator.num_pages— Total number of pages (e.g., 50).page_obj.paginator.count— Total number of records (e.g., 500).page_obj.has_previous— True if there is a page before the current one.page_obj.has_next— True if there is a page after the current one.page_obj.previous_page_number— The page number before the current one.page_obj.next_page_number— The page number after the current one.page_obj.object_list— The actual queryset/list for the current page.
Displaying Page Number Links
Instead of just Previous/Next buttons, you can show numbered page links. Use paginator.page_range to loop through all page numbers:
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<strong>{{ num }}</strong> <!-- Highlight current page -->
{% else %}
<a href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
Example: This generates page number links like: 1 2 3 4 5 — where 3 is bold because it is the current page, and the others are clickable links. Just like numbered page tabs in a school register.
Pagination with Filters and Search
When you combine pagination with filtering (like a search box), you must keep the search term in the URL when changing pages. If you don't, clicking "Next Page" clears the search results.
def student_list(request):
query = request.GET.get('q', '') # Get search term
students = Student.objects.filter(name__icontains=query).order_by('name')
paginator = Paginator(students, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'student_list.html', {
'page_obj': page_obj,
'query': query
})
In the template, append the search query to the pagination links:
<!-- Search box -->
<form method="get">
<input type="text" name="q" value="{{ query }}" placeholder="Search students...">
<button type="submit">Search</button>
</form>
<!-- Pagination links — include q= so search is preserved -->
{% if page_obj.has_previous %}
<a href="?q={{ query }}&page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
{% if page_obj.has_next %}
<a href="?q={{ query }}&page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
Pagination with Class-Based ListView
If you are using Class-Based Views, pagination is even simpler. Just add paginate_by:
from django.views.generic import ListView
from .models import Student
class StudentListView(ListView):
model = Student
template_name = 'student_list.html'
context_object_name = 'students'
paginate_by = 10 # <-- That's all you need!
ordering = ['name']
Django's ListView automatically handles the pagination logic and passes page_obj and is_paginated to the template. Your pagination template code remains exactly the same as before.
Handling Invalid Page Numbers
What if someone types ?page=9999 and that page does not exist? The get_page() method handles this gracefully — it returns the last valid page instead of crashing. This is safer than using paginator.page(), which raises an error for invalid pages.
# get_page() is safe — returns last page if out of range, page 1 if invalid
page_obj = paginator.get_page(page_number) # Recommended ✓
# page() is strict — raises InvalidPage exception if out of range
page_obj = paginator.page(page_number) # Use only if you handle exceptions
Quick Recap
- Use Django's
Paginatorclass to split large QuerySets into pages. Paginator(queryset, items_per_page)— creates a paginator object.paginator.get_page(page_number)— gets the records for a specific page safely.- Get the current page from the URL with
request.GET.get('page'). - Use
page_obj.has_previousandpage_obj.has_nextto show navigation buttons. - When combining with search, include the search term in pagination links.
- For Class-Based Views, simply add
paginate_by = 10— no extra code needed.
