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

Asked By: Vince M

||

Answers:

You can use Protocols:

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.

Answered By: hoefling
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.