Pagination

Pagination is the technique of returning a large result set in small, numbered slices — a page at a time — instead of all rows at once, keeping responses fast and manageable.

Learn Pagination in our free Flask course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Flask course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll work the offset/limit math, compute total pages and prev/next flags, build a JSON paginated API response, and apply the same logic with SQLAlchemy.

Pagination boils down to two numbers the client sends: page (which slice) and per_page (how big). From those you compute everything else. The offset — how many items to skip — is (page - 1) * per_page . The total pages is the item count divided by per_page , rounded up.

The runnable example builds a paginate() helper over a plain list of 23 items and asks for page 2 of 10-per-page.

Output: page 2 of 3 shows item-11 … item-20 with both flags true; page 3 shows the final three items and has_next: False .

In a real API the client passes ?page=2&per_page=10 as query parameters, and you return JSON with both the items and the navigation metadata. The example wraps the math in a Flask route, reads the params with sensible defaults, and returns a structured response — tested with the in-process client.

Output: page: 2 of 3 , the ten product names for that page, and has_next: True . The same shape works for any list-style API.

Slicing a Python list is fine for small data, but with thousands of rows you want the database to return only the page you need. That is exactly what .limit(per_page).offset(offset) does — plus one cheap count() to know the total.

Output: total: 23 pages: 3 and page 2 names: ['P11', …, 'P20'] — only ten rows ever leave the database.

Flask-SQLAlchemy bundles all of this into one call. Here is the production shortcut:

Complete the paginator. Replace each ___ to compute the offset, total pages, and next flag.

Pages are 1-based but offsets are 0-based. Use (page - 1) * per_page , not page * per_page , or page 1 will skip the first batch.

Query params are strings. Read them with request.args.get("page", 1, type=int) so the math doesn't break.

Use math.ceil(total / per_page) . Plain // truncates, hiding a final partial page of results.

Extend a paginated response with prev/next page numbers.

Lesson complete — your APIs stay fast at any size!

You can compute offsets and total pages, set has_prev / has_next , return clean paginated JSON, and push the slicing down into the database with limit and offset .

🚀 Up next: CORS & Cross-Origin Requests — let a front-end on another domain call your API safely.

Practice quiz

How do you compute the offset for a given page?

  • (page - 1) * per_page
  • page * per_page
  • page + per_page
  • per_page / page

Answer: (page - 1) * per_page. Pages are 1-based, so page 1 must skip 0 items: (page - 1) * per_page.

What does per_page control?

  • The total row count
  • How many items each page returns
  • The current page number
  • The sort order

Answer: How many items each page returns. per_page is the limit — how many items appear on a single page.

How do you compute total pages so a final partial page still counts?

  • total // per_page
  • round(total / per_page)
  • math.ceil(total / per_page)
  • math.floor(total / per_page)

Answer: math.ceil(total / per_page). math.ceil rounds up so a leftover partial page is counted as a full page.

When is has_next true?

  • Always
  • When page equals 1
  • When per_page is large
  • When page < total_pages

Answer: When page < total_pages. There is a next page only while the current page is below the total page count.

At the database level, which methods return just one page of rows?

  • .limit(per_page).offset(offset)
  • .all().slice()
  • .filter(page)
  • .group_by(page)

Answer: .limit(per_page).offset(offset). .limit() plus .offset() makes the database return only the needed window.

How should you read the page query parameter as an integer?

  • int(request.page)
  • request.args.get("page", 1, type=int)

Answer: request.args.get("page", 1, type=int). Query params are strings; passing type=int converts page to an integer with a default.

What does Flask-SQLAlchemy's query.paginate() return?

  • A plain list of rows
  • A SQL string
  • A Pagination object with .items, .pages, .total
  • A tuple (page, per_page)

Answer: A Pagination object with .items, .pages, .total. paginate() returns a Pagination object exposing .items, .page, .pages, .total, has_next/has_prev.

Why is has_prev false on page 1?

  • per_page is zero
  • total_pages is unknown
  • The list is empty
  • Because page > 1 is False

Answer: Because page > 1 is False. has_prev is page > 1, which is False on the first page.

What bug does using page * per_page (instead of (page-1)*per_page) cause?

  • Page 1 skips the first batch of items
  • Pages load twice
  • The total count is wrong
  • It raises a SyntaxError

Answer: Page 1 skips the first batch of items. Without the -1, page 1 offsets by per_page and skips the first set of rows.

Why paginate instead of returning every row at once?

  • It changes the sort order
  • Responses stay small and fast
  • It encrypts the data
  • It removes duplicates

Answer: Responses stay small and fast. Sending a fixed-size slice keeps responses fast and avoids overwhelming the client.