How to type a function with Callable without loosing keyword argument?
Question:
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 loose 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
Answers:
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.
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 loose 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.