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.

Note: These are correct, real Flask examples, but this website's own project doesn't run a Flask server, so nothing here executes live on the page — read each example alongside the explanation, then try it in your own project.

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.

ConverterAcceptsExample route
string (default)Any text that does not contain a slash/shelves/<name>
intPositive integers/books/<int:book_id>
floatPositive floating-point values/books/<float:rating>
pathLike string, but also accepts slashes/shelves/<path:subpath>
uuidStrings 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).

Shortcut: Flask-SQLAlchemy also offers 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.

Next up: Now that routes can capture data from the URL, Request & Response Objects covers everything else a view might need from the incoming request — query strings, submitted form data, and the different ways a view can shape its response.