DataLens

Dynamic routes and URL parameters

Capture parts of the URL as function arguments, convert them to the right type, return 404 for unknown items, and build URLs with url_for.

Level: Foundations · 8 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

A shop with 15 products shouldn't need 15 routes. Instead, one route handles the pattern /products/<id>, and the part in angle brackets arrives in your function as an argument. That's how every real site works: /products/3, /customers/ayu-lestari, /orders/2024/06.

Variables in the path

Toko Kita has many products, and each one needs its own page: /products/1, /products/2 and so on. Writing a separate route for every product would be absurd. Instead, you write one route with a placeholder for the id, and Flask passes whatever is in that spot to your function:

from flask import Flask

app = Flask(__name__)

PRODUCTS = {
    1: {"name": "French Press 600ml", "price": 185000},
    2: {"name": "Pour-over Kettle", "price": 320000},
    3: {"name": "Arabica Gayo 250g", "price": 95000},
}


@app.route("/products/<int:product_id>")
def product(product_id):
    item = PRODUCTS.get(product_id)
    if item is None:
        return f"No product {product_id}", 404
    return f"<h1>{item['name']}</h1><p>Rp {item['price']:,}</p>"


client = app.test_client()
print(client.get("/products/2").get_data(as_text=True))
print(client.get("/products/99").status_code)
print(client.get("/products/abc").status_code)
<h1>Pour-over Kettle</h1><p>Rp 320,000</p>
404
404

Converters

ConverterMatchesExample
<name> or <string:name>any text without / (the default)/customers/ayu
<int:name>whole numbers/orders/41
<float:name>decimals/discount/0.15
<path:name>text including //files/reports/2024/june.csv

Converters are free validation: with <int:...>, your function never receives "abc".

Several parameters

A URL can carry more than one value. A monthly report needs both a year and a month, like /reports/2024/7, and each placeholder becomes its own argument:

from flask import Flask

app = Flask(__name__)


@app.route("/reports/<int:year>/<int:month>")
def monthly_report(year, month):
    if not 1 <= month <= 12:
        return "Month must be 1 to 12", 404
    return f"Report for {year}-{month:02d}"


client = app.test_client()
print(client.get("/reports/2024/6").get_data(as_text=True))
print(client.get("/reports/2024/13").status_code)
Report for 2024-06
404

The parameter names in the route must match the function's parameter names.

abort: stop with an error status

Returning "...", 404 works, but abort(404) reads better and stops the function immediately, wherever you are:

from flask import Flask, abort

app = Flask(__name__)
CUSTOMERS = {"ayu-lestari": "Ayu Lestari", "budi-santoso": "Budi Santoso"}


@app.route("/customers/<slug>")
def customer(slug):
    name = CUSTOMERS.get(slug) or abort(404)
    return f"<h1>{name}</h1>"


client = app.test_client()
print(client.get("/customers/ayu-lestari").get_data(as_text=True))
print(client.get("/customers/nobody").status_code)
<h1>Ayu Lestari</h1>
404

The text in the URL (ayu-lestari) is called a slug: a readable, URL-safe version of a name. Slugs make nicer URLs than ids, but they must be unique.

url_for: never hard-code URLs

Links between pages should be built with url_for, from the function name and its parameters:

from flask import Flask, url_for

app = Flask(__name__)


@app.route("/products/<int:product_id>")
def product(product_id):
    return f"Product {product_id}"


@app.route("/")
def home():
    link = url_for("product", product_id=3)
    return f'<a href="{link}">Our best coffee</a>'


client = app.test_client()
print(client.get("/").get_data(as_text=True))
<a href="/products/3">Our best coffee</a>

If you later change the route to /shop/items/<int:product_id>, every url_for("product", ...) link updates automatically. Hard-coded "/products/3" strings would all break.

Key idea

Routes describe URL patterns; view functions receive the variable parts as typed arguments; url_for builds URLs from function names. Your Python code talks in functions and ids, never in hand-built URL strings.

Trailing slashes

@app.route("/products/") (with a slash) also answers /products by redirecting to the slash version. @app.route("/about") (without) returns 404 for /about/. Pick one style and stick to it: folders-like collections with a slash, single pages without.

Why this matters for clean code

Dynamic routes keep one function per kind of page, not per page, which is the "don't repeat yourself" principle at the URL level. Converters validate input before your code runs, abort keeps the happy path flat, and url_for removes a whole class of broken-link bugs.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: HTTP methods: GET and POST →