Get AI summaries of any video or article — Sign up free
Python Flask Tutorial: Full-Featured Web App Part 9 - Pagination thumbnail

Python Flask Tutorial: Full-Featured Web App Part 9 - Pagination

Corey Schafer·
4 min read

Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

Replace `post.query.all()` with `post.query.paginate(page=..., per_page=...)` to return a pagination object rather than a full list.

Briefing

Pagination becomes the centerpiece of the app’s next upgrade: instead of loading every post at once, the home feed switches to Flask-SQLAlchemy’s paginate workflow so only a fixed number of posts render per page, with navigation links at the bottom. After adding enough sample content to reach 25 total posts, the code replaces a simple `post.query.all()` with `post.query.paginate(...)`, turning the result into a pagination object. That object carries metadata like `page`, `per_page`, `total`, `has_next`, `has_prev`, and—most importantly—`items`, which is what gets looped over to render the posts for the current page. By default, the pagination uses 20 posts per page, but the app quickly moves to a practical `per_page` setting (first 5 for clarity, later adjusted for link density). The current page number is pulled from the URL query string via `request.args.get('page', default=1, type=int)`, which also enforces that non-integer page values trigger a clean error rather than breaking the query.

Once pagination is wired into the route, the template changes follow the same logic shift: the home template stops iterating over `posts` directly and instead iterates over `posts.items`. With that in place, the app can be tested by manually visiting `?page=2`, `?page=5`, and confirming that `?page=6` returns “not found” when the page doesn’t exist. The next step is user-facing: rendering page links. Flask-SQLAlchemy provides `iter_pages()`, which yields page numbers plus `None` placeholders that represent gaps (the ellipses behavior seen on many sites). The template uses a loop over `post.iter_pages(...)` and prints either a clickable page link or an ellipsis when the value is `None`. To avoid overwhelming users with dozens of links, the app narrows the range using `left_edge`, `right_edge`, `left_current`, and `right_current`. It also visually distinguishes the active page by switching the Bootstrap button style from outline to filled when `posts.page == page_num`.

After pagination is stable, the feed order is corrected so the newest posts appear first. That’s done by adding an ordering clause before pagination: `post.query.order_by(post.date_posted.desc()).paginate(...)`. Reloading the home page confirms that “my latest post” moves to the top, aligning the listing with typical expectations.

The final functional addition is a dedicated route for author-specific feeds. Clicking a username now leads to `/user/<username>`, where the app first fetches the user with `user.query.filter_by(username=username).first_or_404()`. It then filters posts by that author, orders them by `date_posted.desc()`, and paginates the results exactly like the home feed. A new `user_post.html` template displays a header like “Posts by <username>” along with the user’s total post count, and the pagination links are updated to point back to the user-specific route (including the username parameter). The result is a paginated, sortable, author-filtered browsing experience that scales beyond a single page of content.

Cornell Notes

The app upgrades its post listing by switching from loading all posts at once to using Flask-SQLAlchemy pagination. A pagination object returned by `paginate()` provides metadata such as `page`, `per_page`, `total`, and—critically—`items`, which is what templates loop over to render the current page’s posts. Pagination links are generated with `iter_pages()`, which includes `None` values used to display ellipses and can be constrained with `left_edge/right_edge` and `left_current/right_current`. The feed is also sorted so newest posts appear first using `order_by(post.date_posted.desc())` before paginating. Finally, a `/user/<username>` route filters posts by author, paginates them, and renders a dedicated template with correct pagination URLs.

How does the code change from “load everything” to pagination without breaking the template loop?

The route replaces `post.query.all()` with `post.query.paginate(...)`, which returns a pagination object instead of a plain list. The template then changes from `for post in posts` to `for post in posts.items`, because the actual posts for the current page live in `items`. The current page is read from the URL query string using `request.args.get('page', default=1, type=int)` and passed into `paginate(page=page, per_page=5)` (or another `per_page` value).

What does `iter_pages()` provide, and why does it include `None` values?

`iter_pages()` yields a sequence of page numbers plus `None` placeholders where the UI should show gaps (commonly rendered as ellipses). In the template, the loop checks `if page_num` to render a link for real page numbers, and uses an `else` block to render an ellipsis when `page_num` is `None`. This prevents printing hundreds of links when many pages exist.

How are pagination links limited so the bottom navigation stays readable?

The template calls `post.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2)` (values shown in the transcript). `left_edge/right_edge` control how many pages appear at the far ends, while `left_current/right_current` control how many pages appear near the current page. This reduces clutter while still giving users context and nearby navigation.

How does the app ensure newest posts appear at the top of the paginated feed?

Before calling `paginate()`, the query adds ordering: `post.query.order_by(post.date_posted.desc()).paginate(...)`. Because the sort is applied before pagination, each page contains the correct slice of the globally sorted results—so the newest post appears first on page 1.

What’s required to add an author-specific paginated feed route?

A new route like `/user/<username>` fetches the user with `user.query.filter_by(username=username).first_or_404()` to return a 404 when the username doesn’t exist. Posts are then filtered by author (`post.query.filter_by(author=user)` or equivalent), ordered by `date_posted.desc()`, and paginated. The dedicated template must update pagination URLs to include the username parameter so page links stay within that author’s feed.

Review Questions

  1. When switching to pagination, what attribute must the template iterate over, and why isn’t `posts` itself directly looped over anymore?
  2. Why does ordering (`order_by(...desc())`) need to happen before `paginate()` to correctly show newest posts on page 1?
  3. How do `iter_pages()` and its `None` values work together to produce ellipses in the pagination UI?

Key Points

  1. 1

    Replace `post.query.all()` with `post.query.paginate(page=..., per_page=...)` to return a pagination object rather than a full list.

  2. 2

    Render posts in templates using `for post in posts.items` because `items` holds only the current page’s records.

  3. 3

    Read the requested page number from the URL query string with `request.args.get('page', default=1, type=int)` to enforce integer page values.

  4. 4

    Generate pagination navigation using `post.iter_pages(...)`, rendering links for real page numbers and ellipses when `iter_pages()` yields `None`.

  5. 5

    Constrain pagination link density with `left_edge/right_edge` and `left_current/right_current` so the UI remains usable with many pages.

  6. 6

    Sort results by newest-first by applying `order_by(post.date_posted.desc())` before calling `paginate()`.

  7. 7

    Add an author-specific route (`/user/<username>`) that filters posts by the selected user, paginates them, and updates pagination URLs to include the username parameter.

Highlights

Pagination turns the query result into a metadata-rich object; `items` is the key field that templates must loop over.
`iter_pages()` yields `None` gaps, enabling ellipses-style pagination without manual page-range math.
Newest-first ordering is achieved by applying `order_by(post.date_posted.desc())` before pagination so every page reflects the correct global sort.
The `/user/<username>` route uses `first_or_404()` for safe user lookup and paginates that user’s posts with correct, username-aware pagination links.

Topics

  • Pagination
  • Flask-SQLAlchemy
  • Template Iteration
  • Sorting Posts
  • Author Routes

Mentioned