MyPy has some issues with Callable
*args
and **kwargs
, particularly concerning decorators as detailed in: https://github.com/python/mypy/issues/1927
Specifically, for a decorator with no parameters which only wraps a function (and does not change its signature), you need the following:
from typing import Any, Callable, cast, TypeVar
FuncT = TypeVar('FuncT', bound=Callable[..., Any])
def print_on_call(func: FuncT) -> FuncT:
def wrapped(*args, **kwargs):
print("Running", func.__name__)
return func(*args, **kwargs)
return cast(FuncT, wrapped)
The cast()
at the end should be unnecessary (MyPy should be able to derive that by the invocation of func
at the end of wrapped
that wrapped is indeed FuncT -> FuncT
). I can live with this until it is fixed.
However this breaks horribly when you introduce decorators with parameters. Consider the decorator:
def print_on_call(foo):
def decorator(func):
def wrapped(*args, **kwargs):
print("Running", foo)
return func(*args, **kwargs)
return wrapped
return decorator
Which is used as so:
@print_on_call('bar')
def stuff(a, b):
return a + b
We may attempt to type it (using the parameterless example endorsed by Guido as a guide) like so:
from typing import Any, Callable, Dict, List, TypeVar
FuncT = TypeVar('FuncT', bound=Callable[..., Any])
def print_on_call(foo: str) -> Callable[[FuncT], FuncT]:
def decorator(func: FuncT) -> FuncT:
def wrapped(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
print("Running", foo)
return func(*args, **kwargs)
return cast(FuncT, wrapped)
return cast(Callable[[FuncT], FuncT], decorator)
This appears to typecheck, but when we use it:
@print_on_call('bar')
def stuff(a: int, b: int) -> int:
return a + b
We get a nasty error:
error: Argument 1 has incompatible type Callable[[int, int], int]; expected <uninhabited>
I'm a little confused as to how this is possible. As discussed in PEP 484, it seems that Callable[[int, int], int]
should be a subtype of Callable[..., Any]
.
I assumed that maybe this was a bad iteration between using the generic across the return type of print_on_call
and a a parameter and return type to decorator
, so I shaved my example down to the bare minimum (although no longer a working decorator, it still should typecheck):
from typing import Any, Callable, Dict, List, TypeVar
FuncT = TypeVar('FuncT', bound=Callable[..., Any])
def print_on_call(foo: str) -> Callable[[FuncT], FuncT]:
return cast(Callable[[FuncT], FuncT], None)
However this still results in the above error. This would be something I'd be okay with just #type: ignore
ing away, but unfortunately as a result of this problem, any function decorated with this decorator has type <uninhabited>
, so you start losing type safety everywhere.
That all said (tl;dr):
How do you type decorators (which do not modify the function's signature) with parameters? Is the above a bug? Can it be worked around?
MyPy version: 0.501 (the latest as of this posting)
nowadays, this is supported by mypy directly:
https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
i.e.
FuncT = TypeVar("FuncT", bound=Callable[..., Any])
def my_decorator(func: FuncT) -> FuncT:
@wraps(func)
def wrapped(*args: Any, **kwargs: Any) -> Any:
print("something")
return func(*args, **kwargs)
return cast(FuncT, wrapped)
Oops! Looks like I didn't search hard enough. There is already an issue and a workaround for this: https://github.com/python/mypy/issues/1551#issuecomment-253978622
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With