Python Flask Tutorial: Full-Featured Web App Part 9 - Pagination
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.
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?
What does `iter_pages()` provide, and why does it include `None` values?
How are pagination links limited so the bottom navigation stays readable?
How does the app ensure newest posts appear at the top of the paginated feed?
What’s required to add an author-specific paginated feed route?
Review Questions
- When switching to pagination, what attribute must the template iterate over, and why isn’t `posts` itself directly looped over anymore?
- Why does ordering (`order_by(...desc())`) need to happen before `paginate()` to correctly show newest posts on page 1?
- How do `iter_pages()` and its `None` values work together to produce ellipses in the pagination UI?
Key Points
- 1
Replace `post.query.all()` with `post.query.paginate(page=..., per_page=...)` to return a pagination object rather than a full list.
- 2
Render posts in templates using `for post in posts.items` because `items` holds only the current page’s records.
- 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
Generate pagination navigation using `post.iter_pages(...)`, rendering links for real page numbers and ellipses when `iter_pages()` yields `None`.
- 5
Constrain pagination link density with `left_edge/right_edge` and `left_current/right_current` so the UI remains usable with many pages.
- 6
Sort results by newest-first by applying `order_by(post.date_posted.desc())` before calling `paginate()`.
- 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.