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}
ConverterMatchesPassed to the view as
string (default)Any text without a slashstr
intNon-negative integerint
floatDecimal numberfloat
pathAny text including slashesstr
uuidFormatted UUIDuuid.UUID
any(...)One of the listed literalsstr
<name> customWhatever regex you declareWhatever to_python returns
  • Converters are validation: /posts/abc does not reach the view, it returns 404. Prefer <int:pk> over <string:pk> and skip the manual cast.
  • to_python must raise ValidationError for input the regex let through; to_url is the inverse used by url_for.
  • Two rules matching the same path is not a start-up error — the first registered wins. Print app.url_map when 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 methods accepts GET only (plus HEAD and OPTIONS, which Werkzeug adds automatically).
  • A path that matches with the wrong method returns 405 Method Not Allowed with an Allow header; a path that matches nothing returns 404. Confusing the two sends you looking in the wrong place.
  • HEAD runs 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 AssertionError at 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=2 above ends up after the ?.
  • url_for needs a request or application context to know the host and the script root. Inside a script or a background job, wrap the call in with app.test_request_context() or with app.app_context().
  • A missing endpoint raises BuildError at 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.

Blueprints and the application factory Configuration and project structure

Last refreshed 2026-09-18.