I have written a decorator which changes the types of some arguments passed to a decorated function.
For example, any argument which was int
should become str
:
from typing import Callable
def decorator(func: Callable) -> Callable:
def wrapper(*args, **kwargs):
modified_args = [str(arg) if isinstance(arg, int) else arg for arg in args]
return func(*modified_args, **kwargs)
return wrapper
@decorator
def my_function(a: int | str, b: int) -> str:
return a + b
result = my_function('foo', 4)
print(result)
Running this code outputs 'foo4'
, as expected.
However, according to mypy
the typing is incorrect:
t.py:12: error: Incompatible return value type (got "Union[int, Any]", expected "str") [return-value]
t.py:12: error: Unsupported operand types for + ("str" and "int") [operator]
t.py:12: note: Left operand is of type "Union[int, str]"
Found 2 errors in 1 file (checked 1 source file)
Is there a way to type decorator
so that the within the body of my_function
, mypy will know that any argument of type int
has been transformed to str
?
A def
declaration must use the type hints of the arguments actually passed into and out of the function itself. That a decorator or similar wrapper processes arguments or return values is of no concern for the innermost function.
The type checker then takes into account decorators/wrappers only to derive the actual type for calls. At most, a type checker will verify that decorator and inner function are consistent; the decorator does not modify the types of the inner function itself.
For example, a function decorated with @contextmanager
must return :Iterator[T]
and it is up to the decorator to transform that to :ContextManager[T]
.
For the specific case, this means my_function
must be annotated with the types it actually handles - namely (str, str) -> str
. This has the added advantage of making it clear what a
and b
mean in the function scope:
@decorator # < this must provide `:str, :str` but can receive whatever it wants
def my_function(a: str, b: str) -> str:
# ^ the parameter types reflect what actually arrives in the function
# v in this expression `a: str` and `b: str` in the signature
return a + b
That the decorated my_function
accepts a: int | str, b: int
is the concern of decorator
, not my_function
.