Flask route decorator type hinting

Viewed 761

I have a simple Flask application with which I've added a decorator to ensure a specific header is present on each request.

import functools
from http import HTTPStatus

import flask
from flask.typing import ResponseReturnType

app = flask.Flask(__name__)


# What type hints should be added to this?
def requires_header(func):
    @functools.wraps(func)
    def check_headers(*args, **kwargs):
        if not flask.request.headers.get("X-Foo"):
            flask.abort(HTTPStatus.NOT_FOUND)

        return func(*args, **kwargs)

    return check_headers


@app.route("/", methods=["GET"])
@requires_header
def root() -> ResponseReturnType:
    return flask.jsonify(success=True)


if __name__ == "__main__":
    flask.run(host=0.0.0.0, port=3000)

What type hints should I add to the requires_header decorator?

2 Answers

If you're running Mypy with the --strict setting, the following is the best way to type this at present. This is very similar to @Samwise's answer, but note that the TypeVar is bound to Callable[..., Any] rather than a bare Callable (Mypy --strict will not allow any unparameterised generics, even as arguments to a TypeVar). We also have to help Mypy out when it comes to the return type of requires_header — it can't verify that we're actually returning the type we say we should be returning, so we help Mypy out with a call to typing.cast.

import functools
from http import HTTPS
from typing import Callable, TypeVar, cast, Any 

import flask

C = TypeVar('C', bound=Callable[..., Any])

def requires_header(func: C) -> C:
    @functools.wraps(func)
    def check_headers(*args: Any, **kwargs: Any) -> Any:
        if not flask.request.headers.get("X-Foo"):
            flask.abort(HTTPStatus.NOT_FOUND)

        return func(*args, **kwargs)

    return cast(C, check_headers)

The above solution obviously has its flaws, however, mainly the number of times we've had to use Any, which should generally be used as little as possible. Python 3.10 has introduced a new feature specifically to help with typing decorators, ParamSpec. Mypy unfortunately does not yet support this feature, but when it does, we will be able to type your decorator like this instead:

import functools
from http import HTTPS
from typing import Callable, TypeVar, cast, ParamSpec 

import flask

P = ParamSpec('P')
R = TypeVar('R')

def requires_header(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def check_headers(*args: P.args, **kwargs: P.kwargs) -> R:
        if not flask.request.headers.get("X-Foo"):
            flask.abort(HTTPStatus.NOT_FOUND)

        return func(*args, **kwargs)

    return cast(Callable[P, R], check_headers)

It may be that using ParamSpec will also do away with the need for the cast in the last line of the decorator — but it's difficult to tell, since Mypy doesn't yet support the feature, so it's sadly not yet usable!

Your decorator takes a callable argument (any kind), and returns a callable of the exact same type. Hence:

from typing import Any, Callable, TypeVar

_C = TypeVar("_C", bound=Callable[..., Any])

def requires_header(func: _C) -> _C:
    ...

Note that this is different from doing something like:

def requires_header(func: Callable) -> Callable:
    ...

since this does not guarantee that the decorated function has the same type as the original function (it effectively becomes two different Callable[..., Any] types, which wrecks type checking for anything that calls that function). Using a TypeVar allows the original type to pass through the decorator unchanged.

Related