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.