Flask pagination: A macro for reusable pagination with SQLAlchemy, Jinja, and Bootstrap 5
Content Index
- Why is pagination necessary in Flask?
- Types of pagination in Flask: API vs HTML views
- Pagination in Flask using SQLAlchemy's paginate()
- Parameters page, per_page and error_out
- The Pagination object and its properties
- How to receive the page number from the URL in Flask
- Creating reusable pagination with Jinja macros and Bootstrap 5
- Integrate pagination in Flask with Bootstrap
- Using iter_pages() and active state
- Pagination in Flask using Flask-Paginate
- paginate() vs Flask-Paginate: which one to choose?
- Best practices for pagination in Flask
- Common errors when implementing pagination
- FAQs
- Conclusion
When you work with Flask, sooner or later you face the same problem: large lists. Users, tasks, products, posts… Showing all records at once is not only a bad user experience, but it also affects application performance and the database. This is where pagination in Flask comes into play.
In this article, I am going to teach you how to implement pagination in Flask professionally, combining backend, templates, and frontend, and leaving the code ready to be reused in any module of the project.
Why is pagination necessary in Flask?
Every time you make a GET request that returns multiple records, Flask has to:
- Query the database
- Serialize the data
- Render HTML or return JSON
- Send the response to the client
When the volume of data grows, returning everything in a single response is no longer viable. In my case, this is very noticeable when you start listing real entities (tasks, logs, users…) and the response time starts to grow without you realizing it.
The solution is to divide the results into pages, showing only a controlled portion of data in each request.
Types of pagination in Flask: API vs HTML views
In Flask, we usually encounter two scenarios:
- REST APIs
- Return JSON
- Use parameters like page and per_page
- Very common in microservices
- HTML Views
- Render Jinja templates
- Need visual controls (Bootstrap, links, active state)
Pagination in Flask using SQLAlchemy's paginate()
If you use Flask-SQLAlchemy, you have a huge advantage: the paginate() method.
Parameters page, per_page and error_out
A typical example in the model would be:
@staticmethod
def all_paginated(page=1, per_page=20):
return Post.query.order_by(Post.created.asc()) \
.paginate(page=page, per_page=per_page, error_out=False)- page: current page number
- per_page: records per page
- error_out=False: avoids errors if the page does not exist
This method returns a Pagination object, not a list.
The Pagination object and its properties
The Pagination object is key to building navigation:
- items: elements of the current page
- page: current page
- pages: total number of pages
- has_prev / has_next
- prev_num / next_num
- iter_pages(): generates the numbering
Passing the full object to the template is much more flexible than passing only items.
How to receive the page number from the URL in Flask
The correct way to do it is through GET parameters:
page = request.args.get('page', 1, type=int)Here are three important details I always apply:
- Default value (1) to avoid None
- Conversion to int to avoid strings
- Direct reading from the URL (?page=2)
This makes pagination safe and predictable.
Creating reusable pagination with Jinja macros and Bootstrap 5
One of the biggest mistakes I see is copying and pasting pagination HTML into every template. As soon as you have two lists, the code starts to smell bad.
The solution is to use Jinja macros, which work like reusable functions using macros.
Here we are going to cover the real case of web applications, where backend and frontend go hand in hand.
The Bootstrap pagination component looks like the following:
templates/macro/pagination.html
<nav>
<ul class="pagination">
<li class="page-item"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>Integrate pagination in Flask with Bootstrap
Bootstrap already gives us the visual component; for that, we add the pagination class:
<ul class="pagination">
<li class="page-item"><a class="page-link">1</a></li>
</ul>If you want to use Tailwind, it's the same, only you must create the style yourself and reference it through a class.
Using iter_pages() and active state
Here are two important decisions:
- I use iter_pages() to generate the numbering automatically
- I compare page != pagination.page to:
- Mark the active page
- Avoid navigating to the same page
This detail improves both the UX and the clarity of the code.
The initial code has 3 main blocks: the previous page, the numbering, and the next page; with this clear, we are going to create a Jinja macro that we can easily reuse in any other module:
my_app\templates\macro\pagination.html
{% macro m_pagination(pagination, url='tasks.index') %}
<nav>
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for(url,page=pagination.prev_num) }}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% endif %}
{% for page in pagination.iter_pages() %}
{% if page != pagination.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for(url,page=page) }}" aria-label="Previous">
<span aria-hidden="true">{{ page }}</span>
</a>
</li>
{% else %}
<li class="page-item active">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">{{ page }}</span>
</a>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for(url,page=pagination.next_num) }}" aria-label="Previous">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endmacro %}Remember that macros are a kind of function available in Jinja through which we can supply parameters, which would be the variable data—in this example, the pagination object and a URL. As a consideration, we made a branch through a conditional to mark the active page and suspend navigation to itself:
{% if page != pagination.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for(url,page=page) }}" aria-label="Previous">
<span aria-hidden="true">{{ page }}</span>
</a>
</li>
{% else %}
<li class="page-item active">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">{{ page }}</span>
</a>
</li>
{% endif %}And from the template:
my_app\templates\dashboard\task\index.html
{% extends "dashboard/master.html" %}
{% from 'macro/pagination.html' import m_pagination %}
***
{% block content %}
<table class="table">
<tr>
<td>Id</td>
<td>Name</td>
<td>Options</td>
</tr>
{% for t in tasks.items %}
***
</table>
{{ m_pagination(tasks) }}
{% endblock %}And we will have:

To use it, for example, from the task list:
my_app\tasks\controllers.py
def index():
return render_template('dashboard/task/index.html', tasks=operations.pagination(request.args.get('page', 1, type=int)))As you can see, we are receiving an argument via the URL; that is, a GET type called page:
request.args.get('page')We indicate a default value to avoid comparing with null values:
request.args.get('page', 1)And the type, which by default is String:
request.args.get('page', 1, type=int)Pagination in Flask using Flask-Paginate
In addition to native paginate(), there is the Flask-Paginate library, which further simplifies the generation of controls.
Installation and basic configuration
$ pip install Flask-PaginateBasic example:
from flask import request, render_template
from flask_paginate import Pagination, get_page_parameter
@app.route('/items')
def items():
page = request.args.get(get_page_parameter(), type=int, default=1)
per_page = 10
total = Item.query.count()
items = Item.query.offset((page - 1) * per_page).limit(per_page).all()
pagination = Pagination(page=page,
per_page=per_page,
total=total,
css_framework='bootstrap5')
return render_template('items.html',
items=items,
pagination=pagination)In the template:
{{ pagination.links }}Flask-Paginate is ideal when:
- You don't use Flask-SQLAlchemy
- You want fast results
- You prefer less control and less of your own code
paginate() vs Flask-Paginate: which one to choose?
| Case | Best option |
|---|---|
| Flask-SQLAlchemy | paginate() |
| Total HTML control | Jinja Macros |
| Quick simplicity | Flask-Paginate |
| Advanced Bootstrap | Custom Macro |
| REST APIs | Manual Pagination |
In real projects, I usually use paginate() + macros. I reserve Flask-Paginate for simpler projects or prototypes.
Best practices for pagination in Flask
- Centralize per_page in configuration
- Pass the Pagination object, not just items
- Use macros to avoid duplicating HTML
- Always control the page parameter
- Don't render thousands of page links
Common errors when implementing pagination
- Doing .all() and then paginating in Python
- Not validating page
- Duplicating pagination HTML
- Not marking the active page
- Mixing business logic in the template
FAQs
- Does Flask have native pagination?
- Not directly, but Flask-SQLAlchemy offers paginate().
- Can I use pagination in APIs and HTML views?
- Yes, the concept is the same; only the way of rendering changes.
- Does Flask-Paginate replace paginate()?
- No, they are different and complementary approaches.
- Is Bootstrap mandatory?
- No, but it greatly facilitates the frontend.
Conclusion
Pagination in Flask is not complicated, but doing it well makes a difference. When you combine paginate(), Jinja macros, and Bootstrap, you get a clean, reusable system ready to grow.
In my experience, spending a little time abstracting pagination from the beginning saves many headaches when the project starts to scale.
I agree to receive announcements of interest about this Blog.
We will create a macro so that we can easily reuse the pagination component in any module that we want to use.