Search code examples
pythonmypypython-typing

Decorator which changes ints to strings with correct type hints


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?


Solution

  • 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.