Índice de contenido
- ¿Por qué es necesaria la paginación en Flask?
- Tipos de paginación en Flask: API vs vistas HTML
- Paginación en Flask usando paginate() de SQLAlchemy
- Parámetros page, per_page y error_out
- El objeto Pagination y sus propiedades
- Cómo recibir el número de página desde la URL en Flask
- Creando una paginación reutilizable con macros Jinja y Bootstrap 5
- Integrar paginación en Flask con Bootstrap
- Uso de iter_pages() y estado activo
- Paginación en Flask usando Flask-Paginate
- paginate() vs Flask-Paginate: ¿cuál elegir?
- Buenas prácticas para paginación en Flask
- Errores comunes al implementar paginación
- FAQs
- Conclusión
Cuando trabajas con Flask, tarde o temprano te enfrentas al mismo problema: listas grandes. Usuarios, tareas, productos, posts… Mostrar todos los registros de golpe no solo es una mala experiencia de usuario, también afecta al rendimiento de la aplicación y a la base de datos. Aquí es donde entra en juego la paginación en Flask.
En este artículo voy a enseñarte cómo implementar paginación en Flask de forma profesional, combinando backend, templates y frontend, y dejando el código preparado para reutilizarse en cualquier módulo del proyecto.
¿Por qué es necesaria la paginación en Flask?
Cada vez que haces una petición GET que devuelve múltiples registros, Flask tiene que:
- Consultar la base de datos
- Serializar los datos
- Renderizar HTML o devolver JSON
- Enviar la respuesta al cliente
Cuando el volumen de datos crece, devolverlo todo en una sola respuesta deja de ser viable. En mi caso, esto se nota muchísimo cuando empiezas a listar entidades reales (tareas, logs, usuarios…) y el tiempo de respuesta empieza a crecer sin darte cuenta.
La solución es dividir los resultados en páginas, mostrando solo una porción controlada de datos en cada request.
Tipos de paginación en Flask: API vs vistas HTML
En Flask solemos encontrarnos dos escenarios:
- APIs REST
- Devuelven JSON
- Usan parámetros como page y per_page
- Muy comunes en microservicios
- Vistas HTML
- Renderizan templates Jinja
- Necesitan controles visuales (Bootstrap, enlaces, estado activo)
Paginación en Flask usando paginate() de SQLAlchemy
Si usas Flask-SQLAlchemy, tienes una ventaja enorme: el método paginate().
Parámetros page, per_page y error_out
Un ejemplo típico en el modelo sería:
@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: número de página actual
- per_page: registros por página
- error_out=False: evita errores si la página no existe
Este método devuelve un objeto Pagination, no una lista.
El objeto Pagination y sus propiedades
El objeto Pagination es clave para construir la navegación:
- items: elementos de la página actual
- page: página actual
- pages: número total de páginas
- has_prev / has_next
- prev_num / next_num
- iter_pages(): genera la numeración
Pasar el objeto completo al template es mucho más flexible que pasar solo items.
Cómo recibir el número de página desde la URL en Flask
La forma correcta de hacerlo es a través de parámetros GET:
page = request.args.get('page', 1, type=int)Aquí hay tres detalles importantes que siempre aplico:
- Valor por defecto (1) para evitar None
- Conversión a int para evitar strings
- Lectura directa desde la URL (?page=2)
Esto hace que la paginación sea segura y predecible.
Creando una paginación reutilizable con macros Jinja y Bootstrap 5
Uno de los mayores errores que veo es copiar y pegar HTML de paginación en cada template. En cuanto tienes dos listados, el código empieza a oler mal.
La solución es usar macros Jinja, que funcionan como funciones reutilizables usando los macros.
Aquí vamos a cubrir el caso real de aplicaciones web, donde backend y frontend van de la mano.
El componente de paginación de Bootstrap luce como el siguiente:
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>Integrar paginación en Flask con Bootstrap
Bootstrap ya nos da el componente visual, para eso, agregamos la clase pagination:
<ul class="pagination">
<li class="page-item"><a class="page-link">1</a></li>
</ul>Si quieres emplear Tailwind, es lo mismo, solo que debes de crear el estilo tu y referenciarlo mediante una clase.
Uso de iter_pages() y estado activo
Aquí hay dos decisiones importantes:
- Uso iter_pages() para generar la numeración automáticamente
- Comparo page != pagination.page para:
- Marcar la página activa
- Evitar navegar a la misma página
Este detalle mejora tanto la UX como la claridad del código.
El codigo inicial, tiene 3 bloques principales, el de la página previa, la numeración y la página siguiente; teniendo esto claro, vamos a crear un macro en Jinja que podamos reutilizar fácilmente en cualquier otro módulo:
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 %}Recuerda que los macros son una especie de funciones que tenemos disponibles en Jinja y mediante la misma podemos suministrar parámetros, que seria la data variable, en este ejemplo, el objeto de paginación y una URL; como consideración, hicimos una bifurcación mediante un condicional para marcar la página activa y suspender la navegación a sí misma:
{% 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 %}Y desde el 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 %}Y tendremos:

Para utilizarlo, por ejemplo, desde el listado de tareas:
my_app\tasks\controllers.py
def index():
return render_template('dashboard/task/index.html', tasks=operations.pagination(request.args.get('page', 1, type=int)))Como puedes ver, estamos recibiendo un argumento por la URL; es decir, de tipo GET llamado page:
request.args.get('page')Indicamos un valor por defecto, para evitar comparar con valores nulos:
request.args.get('page', 1)Y el tipo, que por defecto es String:
request.args.get('page', 1, type=int)Paginación en Flask usando Flask-Paginate
Además de paginate() nativo, existe la librería Flask-Paginate, que simplifica aún más la generación de controles.
Instalación y configuración básica
$ pip install Flask-PaginateEjemplo básico:
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)En el template:
{{ pagination.links }}Flask-Paginate es ideal cuando:
- No usas Flask-SQLAlchemy
- Quieres resultados rápidos
- Prefieres menos control y menos código propio
paginate() vs Flask-Paginate: ¿cuál elegir?
| Caso | Mejor opción |
|---|---|
| Flask-SQLAlchemy | paginate() |
| Control total de HTML | Macros Jinja |
| Simplicidad rápida | Flask-Paginate |
| Bootstrap avanzado | Macro personalizada |
| APIs REST | Paginación manual |
En proyectos reales, suelo usar paginate() + macros. Flask-Paginate lo reservo para proyectos más simples o prototipos.
Buenas prácticas para paginación en Flask
- Centraliza per_page en configuración
- Pasa el objeto Pagination, no solo items
- Usa macros para no duplicar HTML
- Controla siempre el parámetro page
- No renderices miles de enlaces de página
Errores comunes al implementar paginación
- Hacer .all() y luego paginar en Python
- No validar page
- Duplicar el HTML de paginación
- No marcar la página activa
- Mezclar lógica de negocio en el template
FAQs
- ¿Flask tiene paginación nativa?
- No directamente, pero Flask-SQLAlchemy ofrece paginate().
- ¿Puedo usar paginación en APIs y vistas HTML?
- Sí, el concepto es el mismo; cambia la forma de renderizar.
- ¿Flask-Paginate reemplaza a paginate()?
- No, son enfoques distintos y complementarios.
- ¿Bootstrap es obligatorio?
- No, pero facilita mucho el frontend.
Conclusión
La paginación en Flask no es complicada, pero hacerla bien marca la diferencia. Cuando combinas paginate(), macros Jinja y Bootstrap, obtienes un sistema limpio, reutilizable y preparado para crecer.
En mi experiencia, dedicar un poco de tiempo a abstraer la paginación desde el principio ahorra muchos dolores de cabeza cuando el proyecto empieza a escalar.