I have many functions and I want to add a stringify option on each function: if stringify=True
, the functions returns str(result)
instead of result
.
Instead of adding the optional argument and refactoring each function (which would force me to duplicate the stringify logic across all my functions) I used a decorator:
from typing import Callable, Union
def WithStringifyOption(func: Callable[[int], int]) -> Callable[[int, bool], Union[int, str]]:
def wrapper(value: int, stringify: bool = False) -> Union[int, str]:
result = func(value)
return str(result) if stringify else result
return wrapper
# my old function
def func0(value: int) -> int:
return value * 2
# How it looks now
@WithStringifyOption
def func1(value: int) -> int:
return value * 2
func0(value=2)
func1(value=3, stringify=True)
The code works, but by doing this, I get typing error (and I lose autocomplete for keyword arguments): Unexpected keyword argument "value" for "func1"mypy(error)
and Unexpected keyword argument "stringify" for "func1"mypy(error)
According to typing documentation about typing.Callable
:
There is no syntax to indicate optional or keyword arguments; such function types are rarely used as callback types.
So how can I fix this typing error ?
Note: this is a simplified example on my real project; i want to add a much more complex logic on each one of my functions, so refactoring all my functions by adding and duplicating this complex logic is not an option. I also don't want to add type: ignore
everywhere on the project and i don't want to replace all keyword arguments by positional arguments everywhere on the project
You can use Protocol
s:
from typing import Protocol
# if sys.version_info < (3, 8): from typing_extensions import Protocol
class CallableIn(Protocol):
def __call__(self, value: int) -> int: ...
class CallableOut(Protocol):
def __call__(self, value: int, stringify: bool = False) -> int | str: ...
def WithStringifyOption(func: CallableIn) -> CallableOut:
def wrapper(value: int, stringify: bool = False) -> int | str:
result = func(value)
return str(result) if stringify else result
return wrapper
Try out your snippet in mypy playground to check.
typing.Literal
sIf you have a recent version of Python (3.8 or newer), you can improve the CallableOut
signature by overloading the __call__
method with specific literal values for stringify
, leading to more specific return type:
from typing import overload
class CallableOut(Protocol):
@overload
def __call__(self, value: int, stringify: Literal[False] = ...) -> int: ...
@overload
def __call__(self, value: int, stringify: Literal[True] = ...) -> str: ...
def __call__(self, value: int, stringify: bool = False) -> int | str: ...
Continuing your example, you will now get the correct return types for the decorated func1
based on the stringify
values:
reveal_type(func1(value=3)) # note: Revealed type is "builtins.int"
reveal_type(func1(value=3, stringify=False)) # note: Revealed type is "builtins.int"
reveal_type(func1(value=3, stringify=True)) # note: Revealed type is "builtins.str"