Flask allows you to have a very flexible mapping of URL pathes to function calls. Let's see what can you do.

A few notes for placeholders:

  • Simple <varname> captures anything except slashes.
  • <string:varname> is the default prefix so we don't really need to include it. It captures everything except a slash /
  • <int:varname> accepts various unicode digits as well
  • <float:varname> accpets a floating point numnber like 123.4, but it does not accept 1. or .1 or a simple integer like 42 without any dot.
  • <path:varname> accepts any character, including slashes /.

This a sample Flask file with several possible path mappings.

Including one where we created our own rule to match routes that contain a valid IPv4 address.

examples/flask/routes/app.py

from flask import Flask
demoapp = Flask(__name__)

@demoapp.route("/")
def main():
    return "Main page"

@demoapp.route("/some/path")
def some_path():
    return "A fixed path"

@demoapp.route("/user/<name>")
def user_name(name):
    return "The name is {}".format(name)


@demoapp.route("/title/<string:name>")
def title(name):
    return "The title is {}".format(name)

@demoapp.route("/id/<int:uid>")
def user_id(uid):
    return "The uid is {}".format(uid)

@demoapp.route("/coord-x/<float:x>")
def coord_x(x):
    return "The x coordinate is {}".format(x)

@demoapp.route("/place/<path:location>")
def place(location):
    return "The location is {}".format(location)

@demoapp.route("/coord/<float:x>/<float:y>")
def coord(x, y):
    return "The coordinate is ({}, {})".format(x, y)

@demoapp.route("/street/<name>/zip/<code>")
def machine(name, code):
    return "The input is {} and {}".format(name, code)


import converters
demoapp.url_map.converters['ipv4'] = converters.IPv4Converter

@demoapp.route('/ip/<ipv4:address>')
def ip_address(address):
    return "The IP is {}".format(address)

examples/flask/routes/converters.py
from werkzeug.routing import BaseConverter, ValidationError
import re

class IPv4Converter(BaseConverter):
    def to_python(self, value):
        match = re.search(r'(\d+)\.(\d+)\.(\d+)\.(\d+)$', value)
        if not match:
            raise ValidationError()
        for i in [1, 2, 3, 4]:
            if not 0 <= int(match.group(i)) <= 255:
                raise ValidationError()
        return value

You can start the application by running

FLASK_APP=app FLASK_DEBUG=1 flask run

And then you can access it using some of the following URLs:

http://localhost:5000/
http://localhost:5000/some/path
http://localhost:5000/user/foobar
http://localhost:5000/user/          Not Found!

Feel free to try anything you like. Below you'll find the test cases that will help you see which route matches which URLs.

You can use more than one placeholders in a route definition and you can even have fixed elements between the placeholders, though I am not sure when would you want to have that.

Testing the routes

I've also included sample code that will test each one of the given routes.

examples/flask/routes/test_app.py

import app

def test_static_routes():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/')
    assert rv.status == '200 OK'
    assert b'Main page' == rv.data


    rv = myapp.get('/some/path')
    assert rv.status == '200 OK'
    assert b'A fixed path' == rv.data


def test_one_name():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/user/foobar')
    assert rv.status == '200 OK'
    assert b'The name is foobar' == rv.data

    rv = myapp.get('/user/foo-bar .$')   # accepts any character
    assert rv.status == '200 OK'
    assert b'The name is foo-bar .$' == rv.data

    rv = myapp.get('/user/foo/bar')      # except a slash
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data
    print(rv.data)

    rv = myapp.get('/user/')            # and there must be *something*
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data
    print(rv.data)

def test_one_string():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/title/Hello World!')
    assert rv.status == '200 OK'
    assert b'The title is Hello World!' == rv.data


def test_one_int():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/id/42')
    assert rv.status == '200 OK'
    assert b'The uid is 42' == rv.data

    rv = myapp.get('/id/0')
    assert rv.status == '200 OK'
    assert b'The uid is 0' == rv.data

    rv = myapp.get('/id/x42')            # only accepts digits
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data
    print(rv.data)

    rv = myapp.get('/id/-1')             # not even something that looks a negative int
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data

    rv = myapp.get('/id/1.2')            # or a dot
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data


    rv = myapp.get('/id/%D9%A4')         # ٤  ARABIC-INDIC DIGIT FOUR
    assert rv.status == '200 OK'
    assert b'The uid is 4' == rv.data

    rv = myapp.get('/id/٤')              # ٤  ARABIC-INDIC DIGIT FOUR
    assert rv.status == '200 OK'
    assert b'The uid is 4' == rv.data

    rv = myapp.get('/id/߅')              # NKO DIGIT FIVE
    assert rv.status == '200 OK'
    assert b'The uid is 5' == rv.data

def test_float():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/coord-x/4.2')
    assert rv.status == '200 OK'
    assert b'The x coordinate is 4.2' == rv.data

    rv = myapp.get('/coord-x/42')        # does not accept simple digits
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data

    rv = myapp.get('/coord-x/1.2.3')     # nor more than one dot
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data

    rv = myapp.get('/coord-x/.2')
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data

    rv = myapp.get('/coord-x/2.')
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data

def test_path():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/place/a/b/c')
    assert rv.status == '200 OK'
    assert b'The location is a/b/c' == rv.data

    rv = myapp.get('/place/a')
    assert rv.status == '200 OK'
    assert b'The location is a' == rv.data

    rv = myapp.get('/place/foo/bar/')
    assert rv.status == '200 OK'
    assert b'The location is foo/bar/' == rv.data

def test_multiple():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/coord/4.2/1.3')
    assert rv.status == '200 OK'
    assert b'The coordinate is (4.2, 1.3)' == rv.data

def test_strange_multiple():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/street/foo/zip/42')
    assert rv.status == '200 OK'
    assert b'The input is foo and 42' == rv.data

def test_ip_address():
    myapp = app.demoapp.test_client()

    rv = myapp.get('/ip/1.2.3.4')
    assert rv.status == '200 OK'
    assert b'The IP is 1.2.3.4' == rv.data

    rv = myapp.get('/ip/1.2.3')
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data

    rv = myapp.get('/ip/1.2.0.256')
    assert rv.status == '404 NOT FOUND'
    assert b'404 Not Found' in rv.data

As they are writtent these two files need to be in the same directory. You can cd in that directory on your terminal and run:

pytest

If you run it with the -s flag then you'll also see the output from the print statements in the test file.

pytest -s

See also