Type annotations for decorators

Question:

It is not a big problem, but I just wondered the way to solve this.
Since I am new to using function annotations on Python, I am not familiar with it. And I have a question below.

When you make a decorator and want to put the annotation on it, how do you do that?

For example, codes like below.

def decorator(func: Callable[[*args,**kwargs], <what type should be here?>]) -> <??>:
    def new_func(*args, **kwargs):
        return func(*args, **kwargs)
    return new_func
Asked By: Taiki Okano

||

Answers:

Update

I actually think @Nikola Benes has the correct answer instead of me, namely:

PEP 612 introduced ParamSpec, which provides the ability to define dependencies between the parameters of callables.

Below is one way you could have tried to do it before ParamSpec, but ParamSpec is the way to go.

For those using Python <3.10, you should be able to get ParamSpec it from typing_extensions

from typing_extensions import ParamSpec

but I’ve not experimented with it. It might also depend on whether your static type checker (e.g. mypy, pyright, etc.), and the version of that checker, has implemented support for it.

The first part of the PyCon 2022 Typing Summit video recording shows ParamSpec in action.


Old Workaround:

Use Any for the return type and return another Callable of return type Any. From PEP 484 and the python standard library, the first parameter to Callable must be the types of the arguments to the callable, not the arguments themselves. Hence, your use of *args and **kwargs in Callable is unaccepted. Instead, you must use ellipses ... (which permits any number of positional and keyword argument types).

Decorator functions are more cleanly expressed using generic types (typing.TypeVar). In layman’s terms, a generic is something that allows a type to be a parameter.

Paraphrasing from the mypy docs (FYI: mypy is a static type checker package for python):

Decorator functions can be expressed using generic types. Generics can
be restricted to using values that are subtypes of specific types with
the keyword argument bound=.... An upper bound can be used to
preserve the signature of the wrapper function a decorator decorates.

Hence, your example becomes this:

from typing import Any, Callable, TypeVar, cast

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

def decorator(func: F) -> F:
    def new_func(*args, **kwargs):
        return func(*args, **kwargs)
    return cast(F, new_func)

Also paraphrasing from the mypy docs and PEP 484:

The bound on F is used so that calling the decorator on a
non-function will be rejected. Also, the wrapper function (new_func)
is not type-checked because there is (currently) no support for
specifying callback signatures with a variable number of arguments of
a specific type, so we must cast the type at the end.

Answered By: A. Hendry

Note that PEP 612 (which is implemented in Python 3.10) introduces ParamSpec, which solves your problem like this:

from typing import Callable, TypeVar, ParamSpec

T = TypeVar('T')
P = ParamSpec('P')

def decorator(func: Callable[P, T]) -> Callable[P, T]:
    def new_func(*args: P.args, **kwargs: P.kwargs) -> T:
        return func(*args, **kwargs)
    return new_func
Answered By: Nikola Benes
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.