Construir

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.

← Más de Construir