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.
By the end of this tutorial, you will understand:
PaginatorBefore starting, you should already know:
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:
Pagination is useful because it:
It is especially important for:
Django provides a built-in class called Paginator.
Import it like this:
from django.core.paginator import Paginator
It takes two main arguments:
Paginatorviews.pyfrom 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})posts gets all postsPaginator(posts, 5) splits them into pages of 5 itemsrequest.GET.get('page') gets the current page number from the URLget_page(page_number) returns the requested page safelypage_obj is sent to the templatePagination usually works with query parameters.
Examples:
/posts/?page=1
/posts/?page=2
/posts/?page=3Django reads the page number from:
request.GET.get('page')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.
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>has_previous checks if a previous page existsprevious_page_number gets the previous page numbernumber is the current pagepaginator.num_pages is the total number of pageshas_next checks if a next page existsnext_page_number gets the next page numberMany 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.
views.pyfrom 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>get_page() HelpsDjango 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=abcor
?page=9999get_page() handles it gracefully.
For beginners, get_page() is usually the better choice.
Pagination is even easier with class-based views.
ListViewfrom 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']paginate_by = 5 tells Django to show 5 items per pageWith ListView, the template gets:
page_objis_paginatedpaginatorposts 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 %}views.pyfrom 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.pyfrom 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 %}A common issue happens when pagination is combined with search.
Example search URL:
/posts/?q=django&page=2When 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.
views.pyfrom 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,
})<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>Here are useful pagination values:
page_obj.number → current page numberpage_obj.has_previous → whether previous page existspage_obj.has_next → whether next page existspage_obj.previous_page_number → previous page numberpage_obj.next_page_number → next page numberpage_obj.paginator.num_pages → total page countpage_obj.paginator.count → total object countIf 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.
Wrong:
{% for post in posts %}when using FBV pagination that only sent page_obj.
Correct:
{% for post in page_obj %}page from the URLYou need:
page_number = request.GET.get('page')This breaks pagination when using search.
page() instead of get_page() without handling errorsThis can raise exceptions.
get_page() for safer handlingA common page size is:
Imagine a blog with 53 posts and you want to show 6 per page.
views.pyfrom 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})This makes the blog much cleaner and easier to navigate.
In this tutorial, you learned that:
Paginator classPaginator manuallypaginate_byPagination is one of the most useful features in real Django applications because large lists are everywhere.
A. Pager
B. Paginator
C. PageManager
D. SplitView
A. id
B. num
C. page
D. index
A. page()
B. get_page()
C. paginate()
D. split_page()
ListView for pagination?A. page_size
B. limit
C. paginate_by
D. items_per_page
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
The next ideal tutorial is:
Tutorial: Search Functionality in Django
Subject: searching records using forms, QuerySets, and filters.