Routing in depth: converters, methods and url_for
Use URL converters well, handle HTTP methods and trailing slashes deliberately, and build every link from an endpoint name instead of a hard-coded path.
Converters and rule matching
A route is a URL rule plus an endpoint. The rule can capture parts of the path, and the converter you name decides both the regular expression that matches and the Python type your view receives. Werkzeug compiles all rules into one matcher at start-up, so a request is resolved in a single pass rather than by scanning handlers in order.
from flask import Flask
from werkzeug.routing import BaseConverter, ValidationError
app = Flask(__name__)
@app.get("/posts/<int:post_id>")
def show_post(post_id): # post_id arrives as an int, already cast
return {"id": post_id}
@app.get("/files/<path:subpath>") # path: matches slashes too
def show_file(subpath):
return {"path": subpath}
@app.get("/users/<uuid:user_id>")
def show_user(user_id): # a uuid.UUID instance
return {"id": str(user_id)}
@app.get("/export/<any(csv,json):fmt>")
def export(fmt): # any(): an allowlist baked into the rule
return {"fmt": fmt}
@app.get("/blog/<int:year>/<int:month>/")
def archive(year, month):
return {"year": year, "month": month}
# a default for an optional segment
@app.get("/feed/", defaults={"page": 1})
@app.get("/feed/page/<int:page>")
def feed(page):
return {"page": page}
# custom converter: hexadecimal ids, e.g. /item/a1b2c3
class HexConverter(BaseConverter):
regex = r"[0-9a-f]{6}"
def to_python(self, value):
return int(value, 16)
def to_url(self, value):
return f"{int(value):06x}"
app.url_map.converters["hex"] = HexConverter
@app.get("/item/<hex:item_id>")
def item(item_id):
return {"id": item_id}| Converter | Matches | Passed to the view as |
|---|---|---|
string (default) | Any text without a slash | str |
int | Non-negative integer | int |
float | Decimal number | float |
path | Any text including slashes | str |
uuid | Formatted UUID | uuid.UUID |
any(...) | One of the listed literals | str |
<name> custom | Whatever regex you declare | Whatever to_python returns |
- Converters are validation:
/posts/abcdoes not reach the view, it returns 404. Prefer<int:pk>over<string:pk>and skip the manual cast. to_pythonmust raiseValidationErrorfor input the regex let through;to_urlis the inverse used byurl_for.- Two rules matching the same path is not a start-up error — the first registered wins. Print
app.url_mapwhen a request reaches the wrong handler.
HTTP methods and trailing slashes
@app.route("/submit", methods=["GET", "POST"])
def submit():
...
@app.post("/api/posts") # shorthand for methods=["POST"]
def create_post(): ...
@app.get("/posts/<int:pk>")
@app.put("/posts/<int:pk>") # stack decorators for one view
@app.delete("/posts/<int:pk>")
def post_resource(pk): ...
# allow /about and /about/ to be the same rule
@app.get("/about", strict_slashes=False)
def about(): ...
# inspect what exists
for rule in app.url_map.iter_rules():
print(rule, sorted(rule.methods), rule.endpoint)- A rule without
methodsacceptsGETonly (plusHEADandOPTIONS, which Werkzeug adds automatically). - A path that matches with the wrong method returns 405 Method Not Allowed with an
Allowheader; a path that matches nothing returns 404. Confusing the two sends you looking in the wrong place. HEADruns your GET view and discards the body, so keep GET handlers free of side effects.- Registering the same rule twice for different methods on one endpoint is fine; registering the same method twice raises
AssertionErrorat import time.
⚠️
Trailing slashes are part of the URL as far as Flask is concerned.
/posts and /posts/ are two different rules unless you set strict_slashes=False: a request to the version you did not declare is answered with a redirect, and a redirect on a POST depends on the client replaying the method and body. Some HTTP clients and proxies do not, which turns a form submission into a GET of the canonical URL — a silent data loss that only shows up in production. Declare one form, set strict_slashes deliberately, and test both paths.url_for and endpoints
from flask import url_for, redirect, request
with app.test_request_context():
url_for("show_post", post_id=42) # /posts/42
url_for("show_file", subpath="docs/a.pdf") # /files/docs/a.pdf
url_for("show_post", post_id=42, _anchor="top") # /posts/42#top
url_for("show_post", post_id=42, _external=True) # http://host/posts/42
url_for("static", filename="css/app.css") # /static/css/app.css
url_for("show_post", post_id=42, page=2) # /posts/42?page=2 (extra -> query string)
# in a view
@app.post("/submit")
def submit():
return redirect(url_for("show_post", post_id=7))
# in a template
{{ url_for('show_post', post_id=post.id) }}- The endpoint defaults to the view function's name;
endpoint=on the decorator overrides it. Templates should never contain a literal/posts/— rename the rule and every link follows. - Values for converters become path segments; any leftover keyword becomes a query parameter. That is how
page=2above ends up after the?. url_forneeds a request or application context to know the host and the script root. Inside a script or a background job, wrap the call inwith app.test_request_context()orwith app.app_context().- A missing endpoint raises
BuildErrorat render time. Catch it in tests by exercising every template, which is cheaper than finding it from a user report.
FAQ
Why does url_for raise BuildError in a test but work in the browser?
The call is outside a request context, or the endpoint name is wrong. Endpoints registered on a blueprint are prefixed with the blueprint name, for example
shop.show_post, unless the blueprint set url_prefix plus a custom name. Print app.url_map.iter_rules() to see the exact endpoint strings.How do I make a route accept a value with slashes?
Use the
path converter: <path:subpath>. Note that it is greedy, so a rule ending in <path:x> swallows everything after its prefix; put more specific rules before it or add a suffix the converter cannot consume.Related
Blueprints and the application factory Configuration and project structure
Last refreshed 2026-09-18.