0
# Pagination
1
2
Built-in pagination support for both modern SQLAlchemy select statements and legacy Query objects, with request parameter integration and pagination widget helpers.
3
4
## Capabilities
5
6
### Pagination Base Class
7
8
Base class for paginating query results with page navigation and metadata.
9
10
```python { .api }
11
class Pagination:
12
"""
13
Base pagination class for managing paged query results.
14
15
Provides page navigation, item access, and metadata for building
16
pagination widgets in web applications. Don't create instances
17
manually - use db.paginate() or query.paginate() instead.
18
"""
19
20
def __init__(
21
self,
22
page: int | None = None,
23
per_page: int | None = None,
24
max_per_page: int | None = 100,
25
error_out: bool = True,
26
count: bool = True,
27
**kwargs: Any,
28
) -> None:
29
"""
30
Initialize pagination object.
31
32
Parameters:
33
- page: Current page number (from request args if None)
34
- per_page: Items per page (from request args if None)
35
- max_per_page: Maximum allowed per_page value
36
- error_out: Abort with 404 on invalid parameters
37
- count: Calculate total item count
38
- kwargs: Query-specific arguments for subclasses
39
"""
40
41
# Properties for current page state
42
page: int # Current page number
43
per_page: int # Items per page
44
max_per_page: int | None # Maximum allowed per_page
45
items: list[Any] # Items on current page
46
total: int | None # Total items across all pages
47
48
@property
49
def first(self) -> int:
50
"""Number of first item on page (1-based) or 0 if no items."""
51
52
@property
53
def last(self) -> int:
54
"""Number of last item on page (1-based) or 0 if no items."""
55
56
@property
57
def pages(self) -> int:
58
"""Total number of pages."""
59
60
@property
61
def has_prev(self) -> bool:
62
"""True if this is not the first page."""
63
64
@property
65
def prev_num(self) -> int | None:
66
"""Previous page number or None if first page."""
67
68
def prev(self, *, error_out: bool = False) -> Pagination:
69
"""
70
Get Pagination object for previous page.
71
72
Parameters:
73
- error_out: Abort with 404 on invalid page
74
75
Returns:
76
Pagination object for previous page
77
"""
78
79
@property
80
def has_next(self) -> bool:
81
"""True if this is not the last page."""
82
83
@property
84
def next_num(self) -> int | None:
85
"""Next page number or None if last page."""
86
87
def next(self, *, error_out: bool = False) -> Pagination:
88
"""
89
Get Pagination object for next page.
90
91
Parameters:
92
- error_out: Abort with 404 on invalid page
93
94
Returns:
95
Pagination object for next page
96
"""
97
98
def iter_pages(
99
self,
100
*,
101
left_edge: int = 2,
102
left_current: int = 2,
103
right_current: int = 4,
104
right_edge: int = 2,
105
) -> Iterator[int | None]:
106
"""
107
Yield page numbers for pagination widget with smart truncation.
108
109
Parameters:
110
- left_edge: Pages to show from start
111
- left_current: Pages to show left of current
112
- right_current: Pages to show right of current
113
- right_edge: Pages to show from end
114
115
Yields:
116
Page numbers or None for gaps (e.g., 1, 2, None, 7, 8, 9, None, 19, 20)
117
"""
118
119
def __iter__(self) -> Iterator[Any]:
120
"""Iterate over items on current page."""
121
```
122
123
### Select Statement Pagination
124
125
Pagination implementation for modern SQLAlchemy select statements.
126
127
```python { .api }
128
class SelectPagination(Pagination):
129
"""
130
Pagination for SQLAlchemy select statements.
131
132
Used by db.paginate() for modern SQLAlchemy 2.x select() queries.
133
Returned by SQLAlchemy.paginate() method.
134
"""
135
136
def _query_items(self) -> list[Any]:
137
"""Execute paginated select query to get items for current page."""
138
139
def _query_count(self) -> int:
140
"""Execute count query to get total number of items."""
141
```
142
143
### Query Pagination
144
145
Pagination implementation for legacy Query objects.
146
147
```python { .api }
148
class QueryPagination(Pagination):
149
"""
150
Pagination for legacy SQLAlchemy Query objects.
151
152
Used by Query.paginate() for SQLAlchemy 1.x style queries.
153
Returned by Query.paginate() method.
154
"""
155
156
def _query_items(self) -> list[Any]:
157
"""Execute paginated query to get items for current page."""
158
159
def _query_count(self) -> int:
160
"""Execute count query to get total number of items."""
161
```
162
163
## Usage Examples
164
165
### Basic Pagination with Select Statements
166
167
```python
168
@app.route('/users')
169
def list_users():
170
page = request.args.get('page', 1, type=int)
171
172
# Modern SQLAlchemy 2.x style pagination
173
pagination = db.paginate(
174
db.select(User).order_by(User.username),
175
page=page,
176
per_page=20,
177
max_per_page=100
178
)
179
180
return render_template('users.html', pagination=pagination)
181
```
182
183
### Pagination with Query Objects
184
185
```python
186
@app.route('/posts')
187
def list_posts():
188
# Legacy Query style pagination
189
pagination = Post.query.filter_by(published=True).order_by(
190
Post.created_at.desc()
191
).paginate(
192
per_page=10,
193
error_out=False # Use page 1 instead of 404 for invalid pages
194
)
195
196
return render_template('posts.html', pagination=pagination)
197
```
198
199
### Request Parameter Integration
200
201
```python
202
@app.route('/search')
203
def search():
204
query = request.args.get('q', '')
205
page = request.args.get('page', 1, type=int)
206
per_page = min(request.args.get('per_page', 20, type=int), 100)
207
208
# Pagination automatically reads 'page' and 'per_page' from request
209
# if not explicitly provided
210
pagination = db.paginate(
211
db.select(Post).where(Post.title.contains(query)),
212
page=page,
213
per_page=per_page
214
)
215
216
return {
217
'results': [post.title for post in pagination.items],
218
'pagination': {
219
'page': pagination.page,
220
'pages': pagination.pages,
221
'per_page': pagination.per_page,
222
'total': pagination.total,
223
'has_next': pagination.has_next,
224
'has_prev': pagination.has_prev
225
}
226
}
227
```
228
229
### Pagination Templates
230
231
```html
232
<!-- Example Jinja2 template for pagination -->
233
<div class="pagination">
234
{% if pagination.has_prev %}
235
<a href="{{ url_for(request.endpoint, page=pagination.prev_num, **request.args) }}">« Prev</a>
236
{% endif %}
237
238
{% for page in pagination.iter_pages() %}
239
{% if page %}
240
{% if page != pagination.page %}
241
<a href="{{ url_for(request.endpoint, page=page, **request.args) }}">{{ page }}</a>
242
{% else %}
243
<strong>{{ page }}</strong>
244
{% endif %}
245
{% else %}
246
<span>…</span>
247
{% endif %}
248
{% endfor %}
249
250
{% if pagination.has_next %}
251
<a href="{{ url_for(request.endpoint, page=pagination.next_num, **request.args) }}">Next »</a>
252
{% endif %}
253
</div>
254
255
<!-- Display current page items -->
256
<div class="items">
257
{% for item in pagination.items %}
258
<div>{{ item.title }}</div>
259
{% endfor %}
260
</div>
261
262
<!-- Pagination info -->
263
<div class="info">
264
Showing {{ pagination.first }} to {{ pagination.last }} of {{ pagination.total }} items
265
(Page {{ pagination.page }} of {{ pagination.pages }})
266
</div>
267
```
268
269
### Advanced Pagination Controls
270
271
```python
272
@app.route('/products')
273
def products():
274
category = request.args.get('category')
275
sort = request.args.get('sort', 'name')
276
277
query = db.select(Product)
278
279
if category:
280
query = query.where(Product.category == category)
281
282
if sort == 'price':
283
query = query.order_by(Product.price)
284
else:
285
query = query.order_by(Product.name)
286
287
pagination = db.paginate(
288
query,
289
per_page=12,
290
max_per_page=50,
291
count=True # Calculate total for "X of Y" display
292
)
293
294
return render_template('products.html',
295
pagination=pagination,
296
category=category,
297
sort=sort)
298
```
299
300
### Pagination Widget Helper
301
302
```python
303
def generate_page_links(pagination, endpoint, **kwargs):
304
"""Generate pagination links for templates."""
305
links = []
306
307
# Previous page
308
if pagination.has_prev:
309
links.append({
310
'url': url_for(endpoint, page=pagination.prev_num, **kwargs),
311
'text': 'Previous',
312
'current': False
313
})
314
315
# Page numbers with smart truncation
316
for page in pagination.iter_pages():
317
if page is None:
318
links.append({'text': '…', 'current': False})
319
else:
320
links.append({
321
'url': url_for(endpoint, page=page, **kwargs),
322
'text': str(page),
323
'current': page == pagination.page
324
})
325
326
# Next page
327
if pagination.has_next:
328
links.append({
329
'url': url_for(endpoint, page=pagination.next_num, **kwargs),
330
'text': 'Next',
331
'current': False
332
})
333
334
return links
335
```
336
337
### Performance Optimization
338
339
```python
340
@app.route('/users')
341
def list_users():
342
# Disable count query for better performance on large tables
343
pagination = db.paginate(
344
db.select(User).order_by(User.created_at.desc()),
345
per_page=25,
346
count=False # Skip expensive COUNT query
347
)
348
349
# pagination.total will be None
350
# Use has_next/has_prev for navigation instead
351
return render_template('users.html', pagination=pagination)
352
```
353
354
### Custom Pagination Parameters
355
356
```python
357
def custom_paginate(query, page=None, per_page=None):
358
"""Custom pagination with different defaults."""
359
return db.paginate(
360
query,
361
page=page or 1,
362
per_page=per_page or 50, # Higher default
363
max_per_page=200, # Higher maximum
364
error_out=False # Don't 404 on invalid pages
365
)
366
367
@app.route('/items')
368
def list_items():
369
pagination = custom_paginate(
370
db.select(Item).order_by(Item.name),
371
page=request.args.get('page', type=int)
372
)
373
return render_template('items.html', pagination=pagination)
374
```
375
376
## Pagination Properties Reference
377
378
- **page**: Current page number (1-based)
379
- **per_page**: Number of items per page
380
- **total**: Total number of items (None if count=False)
381
- **items**: List of items on current page
382
- **pages**: Total number of pages
383
- **first**: First item number on page (1-based)
384
- **last**: Last item number on page (1-based)
385
- **has_prev**: True if previous page exists
386
- **has_next**: True if next page exists
387
- **prev_num**: Previous page number (None if first page)
388
- **next_num**: Next page number (None if last page)