5. Routing and URL parameters
A Flask app grows by adding routes. Each route gives the browser another URL it can ask for, and each URL runs a Python function. This page covers fixed URLs, URLs with values inside them, and url_for(), the Flask helper that builds links from route names.
Static routes
A static route matches one exact URL. You already wrote the home route:
@app.route("/")
def home():
return "Home page"
You can add more fixed pages the same way:
@app.route("/about")
def about():
return "About this project"
@app.route("/status")
def status():
return "Music Time Machine is running"
Static routes are best when the URL names a page, not a changing piece of data.
Dynamic routes
A dynamic route captures part of the URL and passes it into the function as an argument. Use angle brackets around the changing part:
@app.route("/tracks/<track_id>")
def track_detail(track_id):
return f"Track ID: {track_id}"
If the browser opens /tracks/abc123, Flask runs track_detail("abc123"). The value from the URL becomes a normal Python variable inside the function.
Dynamic routes are useful when the URL identifies a specific thing: one track, one artist, one saved snapshot, one user, or one document.
Type converters
By default, Flask captures URL values as strings. If you need a number, add a type converter:
@app.route("/snapshots/<int:year>/<int:month>")
def monthly_snapshot(year, month):
return f"Snapshot for {year}-{month}"
Now /snapshots/2026/6 passes two integers into the function. If someone opens /snapshots/june/latest, Flask does not run the function because the URL does not match the route. It returns a 404 instead.
Common converters include:
<name>captures a string without slashes.<int:id>captures an integer.<float:value>captures a floating-point number.<path:filename>captures a path that may include slashes.
When to use a dynamic route
Use a dynamic route when the value is required for the page to make sense:
/tracks/<track_id>means "show this track"./artists/<artist_id>means "show this artist"./snapshots/<int:year>/<int:month>means "show this month's snapshot".
Optional filters, searches, and sorting controls belong in query strings, such as /search?q=radiohead. Those come later on the forms and request-data page.
Why hardcoded links become a problem
If you write links by hand, every URL becomes something you have to maintain yourself:
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/status">Status</a>
</nav>
This works at first. The problem appears when a route changes. If /status becomes /health, every hardcoded link to /status has to be found and edited by hand.
Generate links with url_for()
Flask's url_for() builds a URL from the view function name. That means your templates can point at the Python function instead of spelling out the path manually.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Home page"
@app.route("/status")
def status():
return "Music Time Machine is running"
The names passed to url_for() are function names: home and status. If you later change @app.route("/status") to @app.route("/health"), the template can stay the same because the function is still called status.
Build URLs with dynamic values
For dynamic routes, pass the captured values as keyword arguments:
@app.route("/tracks/<track_id>")
def track_detail(track_id):
return f"Track ID: {track_id}"
View track
That generates /tracks/abc123. The route defines the shape of the URL; url_for() fills in the values.
Use url_for() in redirects too
The same helper works in Python code. When one route sends the browser to another route, use redirect(url_for(...)) instead of a hardcoded string:
from flask import redirect, url_for
@app.route("/old-status")
def old_status():
return redirect(url_for("status"))
From this point on, use url_for() for internal links, redirects, and static files. It keeps your app easier to change as the route list grows.