Paginación en Flask: mostrar resultados por páginas
Cómo dividir una lista larga de registros en páginas navegables en Flask, con SQL, enlaces de anterior/siguiente y los detalles que evitan errores en los bordes.
Cuando tu app tiene 20 registros, los muestras todos y ya. Pero cuando tiene 2.000, cargarlos de golpe es lento, pesado y una mala experiencia. La solución es la paginación: mostrar los resultados de a bloques, con enlaces para navegar entre páginas. Aquí la construimos en Flask desde cero, entendiendo la lógica.
La idea en dos números
Toda la paginación se reduce a dos valores. Cuántos elementos por página (digamos 10) y en qué página estás (la 1, la 2…). Con esos dos, calculas qué registros pedir a la base de datos usando LIMIT y OFFSET de SQL:
LIMIT= cuántos traer (los 10 de la página).OFFSET= cuántos saltar antes de empezar (en la página 3, saltas los primeros 20).
La fórmula del salto: OFFSET = (página - 1) * por_página.
La ruta con paginación
from flask import Flask, request, render_template
import sqlite3
app = Flask(__name__)
POR_PAGINA = 10
def get_db():
conn = sqlite3.connect("datos.db")
conn.row_factory = sqlite3.Row
return conn
@app.route("/productos")
def productos():
# Leer la página de la URL (?page=2), por defecto 1
pagina = request.args.get("page", 1, type=int)
if pagina < 1:
pagina = 1
offset = (pagina - 1) * POR_PAGINA
db = get_db()
# Los registros de esta página
filas = db.execute(
"SELECT * FROM productos ORDER BY id LIMIT ? OFFSET ?",
(POR_PAGINA, offset),
).fetchall()
# El total, para saber cuántas páginas hay
total = db.execute("SELECT COUNT(*) FROM productos").fetchone()[0]
db.close()
total_paginas = (total + POR_PAGINA - 1) // POR_PAGINA
return render_template(
"productos.html",
productos=filas,
pagina=pagina,
total_paginas=total_paginas,
)
Hay tres piezas clave aquí. El request.args.get("page", 1, type=int) lee el número de página de la URL (?page=2) y el type=int lo convierte a número, evitando que alguien meta texto y rompa el cálculo. El if pagina < 1 protege contra páginas negativas o cero. Y esta línea merece atención:
total_paginas = (total + POR_PAGINA - 1) // POR_PAGINA
Ese es el truco para redondear hacia arriba sin importar librerías: si tienes 25 productos y muestras de 10 en 10, necesitas 3 páginas (no 2). La fórmula lo calcula bien en todos los casos.
Los enlaces de navegación
En la plantilla, los botones de anterior y siguiente, con la lógica para no salirte de los bordes:
<ul>
{% for producto in productos %}
<li>{{ producto.nombre }}</li>
{% endfor %}
</ul>
<div class="paginacion">
{% if pagina > 1 %}
<a href="/productos?page={{ pagina - 1 }}">← Anterior</a>
{% endif %}
<span>Página {{ pagina }} de {{ total_paginas }}</span>
{% if pagina < total_paginas %}
<a href="/productos?page={{ pagina + 1 }}">Siguiente →</a>
{% endif %}
</div>
Los {% if %} son lo que evita el error clásico: no mostrar “Anterior” en la primera página, ni “Siguiente” en la última. Sin esas comprobaciones, el usuario podría navegar a la página 0 o a una página que no existe, y ver una lista vacía o un error. Controlar los bordes es lo que separa una paginación pulida de una a medio hacer.
Por qué no traer todo y paginar en Python
Un error tentador es traer los 2.000 registros de la base y luego, en Python, mostrar solo 10. No lo hagas. El punto de la paginación es no cargar todo: si haces la selección en SQL con LIMIT/OFFSET, la base de datos te devuelve solo los 10 que necesitas, y todo es rápido aunque tengas millones de registros. Paginar en Python después de traerlo todo anula el beneficio y mantiene el problema de rendimiento.
Con esto tienes paginación real y eficiente. La misma lógica funciona igual con PostgreSQL (que usa idéntica sintaxis de LIMIT/OFFSET), así que si sigues el artículo de conectar Flask a PostgreSQL, solo cambias la conexión y el resto es igual.
¿Quieres añadir números de página (1, 2, 3…) en vez de solo anterior/siguiente, o combinarlo con un buscador? Escríbeme desde contacto y lo vemos.