Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type annotations for decorators

Tags:

python

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
like image 853
Taiki Okano Avatar asked Mar 22 '26 22:03

Taiki Okano


2 Answers

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
like image 94
Nikola Benes Avatar answered Mar 24 '26 13:03

Nikola Benes


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.

like image 23
A. Hendry Avatar answered Mar 24 '26 11:03

A. Hendry



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!