How to add Type Hints to positional-only and keyword-only parameters in Python?

Viewed 20

After the inclusion of PEP 3102 and PEP 570 it is now possible to:

Write functions that take keyword-only parameters:

def divide(*, dividend: float, divisor: float) -> float:
    return dividend / divisor

Write functions that take positional-only parameters:

def summation(a: float, b: float, /) -> float:
    return a + b

Write functions that take positional-only and keyword-only parameters:

def difference(a: float, b: float, /, *, absolute: bool = True) -> float:
    return abs(a - b) if absolute else a - b

Type Hints work fine in all cases above, but if these functions are in a closure or part of a another data structure, Pylance throws warnings for the return types.

Example of putting functions in dictionaries:

container_divide: dict[str, Callable[[float, float], float]] = {
    "divide": divide  # Warning
}

container_summation: dict[str, Callable[[float, float], float]] = {
    "summation": summation  # No Warning
}

container_difference: dict[str, Callable[[float, float, bool], float]] = {
    "difference": difference  # Warning
}

The warnings are:

Expression of type "dict[str, (*, dividend: float, divisor: float) -> float]" cannot be assigned to declared type "dict[str, (float, float) -> float]"
  Type "(*, dividend: float, divisor: float) -> float" cannot be assigned to type "(float, float) -> float"
    Function accepts too many positional parameters; expected 0 but received 2
      Keyword parameter "dividend" is missing in destination
      Keyword parameter "divisor" is missing in destination
Expression of type "dict[str, (a: float, b: float, /, *, absolute: bool = True) -> float]" cannot be assigned to declared type "dict[str, (float, float, bool) -> float]"
  Type "(a: float, b: float, /, *, absolute: bool = True) -> float" cannot be assigned to type "(float, float, bool) -> float"
    Function accepts too many positional parameters; expected 2 but received 3

The same situation happens with closures:

def curry_divide(*, divisor: float) -> Callable[[float], float]:
    def inner_divide(*, dividend: float) -> float:
        return dividend / divisor
    return inner_divide  # Warning


def curry_summation(a: float, /) -> Callable[[float], float]:
    def inner_summation(b: float, /) -> float:
        return a + b
    return inner_summation  # No Warning


def curry_difference(a: float, /) -> Callable[[float, bool], float]:
    def inner_difference(b: float, /, *, absolute: bool = True) -> float:
        result = a - b
        return abs(result) if absolute else result
    return inner_difference  # Warning

The warnings are:

Expression of type "(*, dividend: float) -> float" cannot be assigned to return type "(float) -> float"
  Type "(*, dividend: float) -> float" cannot be assigned to type "(float) -> float"
    Function accepts too many positional parameters; expected 0 but received 1
      Keyword parameter "dividend" is missing in destination
Expression of type "(b: float, /, *, absolute: bool = True) -> float" cannot be assigned to return type "(float, bool) -> float"
  Type "(b: float, /, *, absolute: bool = True) -> float" cannot be assigned to type "(float, bool) -> float"
    Function accepts too many positional parameters; expected 1 but received 2

What would be the proper way to add type hints so that there are no warnings? I am only interested in built-in solutions or using the typing_extensions module. No other external libraries.

1 Answers

Functions with keyword only arguments need to use typing.Protocol (personal view, not necessarily the only way):

from typing import Protocol


class Divide(Protocol):

    def __call__(self, *, dividend: float, divisor: float) -> float: ...


class Difference(Protocol):

    def __call__(self, __a: float, __b: float, /, *, absolute: bool = True) -> float: ...


def divide(*, dividend: float, divisor: float) -> float:
    return dividend / divisor


def difference(a: float, b: float, /, *, absolute: bool = True) -> float:
    return abs(a - b) if absolute else a - b


container_divide: dict[str, Divide] = {
    "divide": divide    # mypy pass
}

container_difference: dict[str, Difference] = {
    "difference": difference    # mypy pass
}
Related