Dynamic Routes & Converters
Static routes like /books only get you so far — a real app needs a route
that responds to any book, any shelf, any user, without you writing a separate
@app.route() for every single row in the database. Flask solves this with
variable URL segments: a piece of the route's path is marked as a
placeholder, Flask extracts whatever value the visitor put there, converts it to the
right Python type, and hands it to your view function as an ordinary argument. This
lesson covers capturing those segments, the built-in converters that control how they're
parsed, combining several of them in one route, and what to do when the value a visitor
asked for simply doesn't exist.
Capturing A Variable Segment From The URL
Wrap a name in angle brackets inside the route string and Flask treats that piece of the path as a variable. Whatever Flask captures is passed into your view function as a parameter with the same name:
# app.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/books/<int:book_id>')
def book_detail(book_id):
book = Book.query.get(book_id)
return render_template('books/detail.html', book=book)
Here book_id is declared with the int converter, so a visit to
/books/7 calls book_detail(7) with a real Python integer, not the
string "7" — no manual type conversion needed inside the view.
Built-in Converter Reference
The part before the colon inside <...> is the converter —
it decides what text is allowed in that segment and what Python type the captured value
becomes. Leave it out and Flask falls back to string.
| Converter | Accepts | Example route |
|---|---|---|
string (default) | Any text that does not contain a slash | /shelves/<name> |
int | Positive integers | /books/<int:book_id> |
float | Positive floating-point values | /books/<float:rating> |
path | Like string, but also accepts slashes | /shelves/<path:subpath> |
uuid | Strings formatted as a UUID | /books/<uuid:public_id> |
For the Bookshelf app, int is by far the most common converter — every
Book, Shelf, and User row is looked up by its
integer primary key.
Multiple Captured Segments In One Route
A single route can capture more than one variable. Flask passes each one to the view as its own argument, matched by name — order in the URL doesn't have to match the order of your function's parameters, though keeping them aligned reads better:
# app.py — a book nested under a specific shelf
@app.route('/shelves/<shelf_slug>/books/<int:book_id>')
def shelf_book_detail(shelf_slug, book_id):
shelf = Shelf.query.filter_by(slug=shelf_slug).first_or_404()
book = Book.query.filter_by(id=book_id, shelf_id=shelf.id).first()
if book is None:
abort(404)
return render_template('books/detail.html', shelf=shelf, book=book)
A visit to /shelves/currently-reading/books/12 captures
shelf_slug='currently-reading' and book_id=12, then the view
confirms book 12 actually belongs to that shelf before rendering it —
useful whenever a resource is only meaningful in the context of its parent.
Manually Triggering A 404 With abort()
Book.query.get(book_id) returns None if no row matches — it
does not raise an error on its own. When a lookup like that comes up empty, call
Flask's abort() function with a status code to stop the view immediately and
hand control to Flask's error-handling machinery:
# app.py
from flask import Flask, abort, render_template
@app.route('/books/<int:book_id>')
def book_detail(book_id):
book = Book.query.get(book_id)
if book is None:
abort(404)
return render_template('books/detail.html', book=book)
Calling abort(404) raises an HTTPException internally, so any
code after it in the view never runs. Flask ships a plain, unstyled 404 page by default —
the next lesson, Error Handling, covers replacing
that with your own Bookshelf-branded template via @app.errorhandler(404).
Book.query.get_or_404(book_id), which combines the lookup and the abort(404) call into a single line — you'll see it used from the SQLAlchemy lessons onward.Writing A Custom URL Converter
The five built-in converters cover most cases, but Flask also lets you register your
own by subclassing werkzeug.routing.BaseConverter. This is handy when a
segment of the URL should only ever hold one of a small, fixed set of values — like a
Book's status field, which is always want_to_read,
reading, or finished:
# app.py
from werkzeug.routing import BaseConverter
class BookStatusConverter(BaseConverter):
regex = r'(want_to_read|reading|finished)'
app.url_map.converters['status'] = BookStatusConverter
@app.route('/books/status/<status:value>')
def books_by_status(value):
books = Book.query.filter_by(status=value).all()
return render_template('books/list.html', books=books)
The converter's regex class attribute defines exactly what text Werkzeug
(the library Flask's routing is built on) will accept for that segment. Register it once
under a name in app.url_map.converters, then use that name like any built-in
converter. With this in place, /books/status/reading matches the route, but
/books/status/lost never even reaches your view — Flask returns a 404 before
your code runs, because the URL didn't match the pattern at all. This is a small piece of
conceptual awareness worth having; most projects go a long time before needing a custom
converter, but it's a clean way to push validation up into the routing layer itself.