
Published 2024-02-10 16:27:45
How to Implement Pagination in Your Flask Application
In web development, displaying large volumes of data on a single page can lead to significant load times and a poor user experience. Pagination solves this by dividing the data across multiple pages, allowing for quicker load times and a cleaner UI. Flask, a lightweight WSGI web application framework in Python, offers straightforward ways to implement pagination, making your web applications more efficient and user-friendly.
Here's what we're going to do: imagine we have a lot of data, like a long list of news articles we got from somewhere on the internet.
We'll use a tool called Faker to create fake news articles. This is a way to practice adding pagination without needing real data.
And just so you know, my web site, devoriales.com show list of articles by using the same pagination code.
Getting Started with Flask
Before diving into pagination, ensure you have Flask installed. If you're new to Flask, it's a micro-framework that lets you build web applications quickly and with minimal setup. Install Flask using pip:
pip install Flask
Set up a basic Flask application if you haven't already. Here's a quick starter:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "Hello, Flask!"
if __name__ == '__main__':
app.run(debug=True)
Install Faker
The Faker library for Python is a popular tool for generating fake data. It is particularly useful in testing and development environments where you need to create dummy data for databases, APIs, or other applications without relying on real data.
With Python and pip, we can install Faker by typing the following command:
pip install Faker
Implementing Pagination
Assuming we have a Flask application with a database (such as SQLite, MySQL, or PostgreSQL), we can use Flask's SQLAlchemy extension for database operations, which includes built-in support for pagination. For our small project, we'll use built-in SQLite database.
First, install Flask-SQLAlchemy:
pip install Flask-SQLAlchemy
The create_fake_data()
function below sets up our app with fake blog posts so we can test and show off our website without needing real articles. We pretend we make an API request to get our data. First, it makes sure we have a place to put these posts. Then, if we don't have any posts yet, it creates 100 pretend articles for us. This way, we can see how our website looks and works with lots of posts, without having to write them ourselves.
Let's add models and add the fake data withcreate_fake_data()
function:
app.py
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from faker import Faker
from flask import request
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
db = SQLAlchemy(app)
fake = Faker()
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
def create_fake_data():
# Check if the table exists and create it if it does not
db.create_all()
# Check if there are already posts in the database
if not Post.query.first():
for _ in range(100): # Adjust the number of posts you would like to have
post = Post(title=fake.sentence(), content=fake.text())
db.session.add(post)
db.session.commit()
print("Added 100 fake posts.")
Querying Data with Pagination
With the model set up, we can now query out database and paginate the results. Flask-SQLAlchemy's paginate
method simplifies this process.
Here's how to modify a route to return paginated results:
@app.route('/')
def home():
page = request.args.get('page', 1, type=int) # Default to page 1 if not specified
posts = Post.query.paginate(page=page, per_page=10, error_out=False)
return render_template('index.html', items=posts.items, pagination=posts)
In this route, paginate
is called on the Item
query object. The page
variable specifies the current page (defaulting to 1 if not specified), and per_page
defines how many items to display on each page. The error_out=False
parameter ensures that an empty list is returned if the page is out of range, rather than throwing an error.
Rendering Pagination in Templates
With the data paginated, the next step is to display it in a flas template. Flask's Jinja2 templating engine makes it easy to loop through items and create pagination controls. Here's an example index.html
template.
To give our site a stylish look, we'll use the Bootstrap framework. It's a popular tool for making websites look good without a lot of work.
First, create a folder named "templates." Inside this folder, you're going to make a new file called "index.html." This is where you'll put the code that shows your paginated data.
As you write the code, you'll see comments (notes) in the code below. These notes are there to help you understand how the pagination works and how we use Jinja2 to make it happen.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Paginated Posts</title>
<!-- Bootstrap CSS for styling the pagination and list elements -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2>Posts</h2>
<!-- List group for displaying each post in a styled list -->
<ul class="list-group mb-4">
<!-- Start looping through each post item provided to the template -->
{% for post in items %}
<li class="list-group-item">
<!-- Display the title of the post -->
<h5>{{ post.title }}</h5>
<!-- Display the content of the post -->
<p>{{ post.content }}</p>
</li>
{% endfor %} <!-- End of the loop -->
</ul>
<!-- Navigation section for pagination controls -->
<nav aria-label="Page navigation example">
<ul class="pagination">
<!-- Check if there is a previous page -->
{% if pagination.has_prev %}
<li class="page-item">
<!-- If there is, create a link to the previous page -->
<a class="page-link" href="/?page={{ pagination.prev_num }}">Previous</a>
</li>
{% else %}
<!-- If not, show a disabled 'Previous' button -->
<li class="page-item disabled"><span class="page-link">Previous</span></li>
{% endif %}
<!-- Loop through each page number provided by pagination.iter_pages() -->
{% for page_num in pagination.iter_pages() %}
<!-- Check if the page number exists (not None) -->
{% if page_num %}
<!-- Check if the current page is not the active page -->
{% if page_num != pagination.page %}
<!-- Create a clickable link for the page number -->
<li class="page-item"><a class="page-link" href="/?page={{ page_num }}">{{ page_num }}</a></li>
{% else %}
<!-- Highlight the current page as active and not clickable -->
<li class="page-item active" aria-current="page">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<!-- For gaps in the pagination links, show ellipsis -->
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
<!-- Check if there is a next page -->
{% if pagination.has_next %}
<li class="page-item">
<!-- If there is, create a link to the next page -->
<a class="page-link" href="/?page={{ pagination.next_num }}">Next</a>
</li>
{% else %}
<!-- If not, show a disabled 'Next' button -->
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
</div>
<!-- Bootstrap Bundle with Popper for responsive and interactive elements -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
This template displays the items for the current page and generates navigation links to move between pages. Conditional logic is used to manage the appearance of "Previous" and "Next" links based on whether previous or next pages exist.
❗Our template knows about things like pagination.has_next
or pagination.has_prev
because we pass the pagination
object to it from our Flask route. This object comes from Flask's SQLAlchemy paginate
method, which includes properties like has_next
and has_prev
. These properties tell us if there are pages of data beyond the current one or before it, making it easy to create "Next" and "Previous" buttons in our web pages.
Since this post is suppose to be beginner friendly, let's break down the templating code:
In our Flask app, we use something called Jinja2 to make our web pages show different content dynamically. It's like a smart template that knows how to show each post on the page. Here's how it works:
We have a list called items
, and each item in this list is a post. We go through this list and for each post, we show its title and content like this:
- We start with
{% for post in items %}
to go through each post in the list. - For each post, we show the title in a big font (
<h5>{{ post.title }}</h5>
) and the content just below it (<p>{{ post.content }}</p>
). - We finish going through the list with
{% endfor %}
.
Adding Page Buttons to Navigate: To help users move between pages of posts, we add navigation buttons like "Previous" and "Next." Here's how we make them work easily with Flask and Jinja2:
- We use a
<nav>
tag to hold our page buttons, making them look good with a little help from Bootstrap. - For the "Previous" button, we check if there's a page before the current one. If there is, we make a clickable "Previous" button. If not, we show a "Previous" button that you can't click.
- We create buttons for each page number. If there are too many pages, we cleverly skip some numbers so it doesn't get too crowded.
- For the "Next" button, it's like the "Previous" button but for going forward. If there's a next page, the button takes you there. If not, it's just there for show and can't be clicked.
Flask Home Route
When a user visits the root URL (/
), the home
function is called. This function determines which page of posts to display based on the page
query parameter in the URL.
The paginate
method is then used to retrieve only a subset of posts from the database, corresponding to the current page. This subset (along with pagination controls like "Next" and "Previous" links) is passed to the index.html
template.
Inside index.html
, Jinja2 templating language is used to:
- Loop through the
items
and display each post. - Use the
pagination
object to dynamically generate pagination controls, allowing the user to navigate between pages of posts.
This setup efficiently handles large sets of posts by only loading and displaying a manageable number at a time, improving the user experience and reducing server load.
Breaking down essentials in home route related to pagination:
-
page = request.args.get('page', 1, type=int)
: This line gets the current page number from the URL's query string (?page=2
). If no page number is specified, it defaults to 1. Thetype=int
ensures the value is an integer. -
posts = Post.query.paginate(page=page, per_page=10, error_out=False)
: Here,Post.query.paginate
is used to fetch a paginated result of posts from the database.page=page
sets the current page number.per_page=10
specifies how many items should be displayed on each page.error_out=False
prevents the app from returning an error if the page number is out of range (e.g., too high). Instead, it will return an empty list.
-
return render_template('index.html', items=posts.items, pagination=posts)
: This renders theindex.html
template, passing in two arguments:items=posts.items
provides the list of posts for the current page.pagination=posts
passes thePagination
object to the template. This object contains information about the current pagination state, such as the total number of pages, the current page number, and whether there are previous or next pages available.
Result
Below is a snapshot of how our website looks now. As you can see, the pagination controls are placed at the bottom, allowing users to navigate through the pages effortlessly. This not only makes the site more user-friendly but also adds a great touch to its appearance. Using Flask's Jinja2 templating engine and Bootstrap, we've transformed a simple data list into an engaging and navigable interface. Pretty nice looking after all:
Full Code
Here is the full code of our project.
app.py:
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from faker import Faker
from flask import request
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
db = SQLAlchemy(app)
fake = Faker()
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
def create_fake_data():
db.create_all()
# Check if there are already posts in the database
if not Post.query.first():
for _ in range(100): # Adjust the number of posts as you wish
post = Post(title=fake.sentence(), content=fake.text())
db.session.add(post)
db.session.commit()
print("Added 100 fake posts.")
@app.route('/')
def home():
page = request.args.get('page', 1, type=int) # Default to page 1 if not specified
posts = Post.query.paginate(page=page, per_page=10, error_out=False)
return render_template('index.html', items=posts.items, pagination=posts)
if __name__ == '__main__':
# Create the database if it does not exist
with app.app_context():
create_fake_data() # Create fake data if the table is empty
app.run(debug=True)
❗Please Note - In Flask, the application context is a special structure that allows for the temporary establishment of an application's context globally. This means that it temporarily makes the application act as if it is handling a request, even when it's actually running a script or a command.
templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Paginated Posts</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2>Posts</h2>
<ul class="list-group mb-4">
{% for post in items %}
<li class="list-group-item">
<h5>{{ post.title }}</h5>
<p>{{ post.content }}</p>
</li>
{% endfor %}
</ul>
<nav aria-label="Page navigation example">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="/?page={{ pagination.prev_num }}">Previous</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Previous</span></li>
{% endif %}
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num != pagination.page %}
<li class="page-item"><a class="page-link" href="/?page={{ page_num }}">{{ page_num }}</a></li>
{% else %}
<li class="page-item active" aria-current="page">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="/?page={{ pagination.next_num }}">Next</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Conclusion
Implementing pagination in your Flask application can significantly improve user experience by making data consumption manageable and interfaces cleaner. By leveraging Flask-SQLAlchemy's pagination features, you can efficiently implement this functionality with minimal code. Always remember to test pagination thoroughly to ensure a seamless user experience across your application's different views.
We hope this guide helps you add pagination to your Flask projects, making your web applications more user-friendly and performant.
About the Author
Aleksandro Matejic, a Cloud Architect, began working in the IT industry over 21 years ago as a technical specialist, right after his studies. Since then, he has worked in various companies and industries in various system engineer and IT architect roles. He currently works on designing Cloud solutions, Kubernetes, and other DevOps technologies.
You can contact Aleksandro by visiting his LinkedIn Profile