Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to type hint a function which takes a callable and its required positional arguments?

Here is my function:

def call_func(func, *args):
    return func(*args)

I think I have two options here:

  1. Using TypeVarTuple -> in Callable[[*Ts], Any] form.

    Ts = TypeVarTuple("Ts")
    T = TypeVar("T")
    
    def call_func(func: Callable[[*Ts], T], *args: *Ts) -> T:
        return func(*args)
    

    Currently Mypy has problem with [*Ts] part. it says: Invalid type comment or annotation. (I also enabled --enable-incomplete-feature=TypeVarTuple.)

  2. Using ParamSpec -> in Callable[P, Any] form.

    P = ParamSpec("P")
    T = TypeVar("T")
    
    def call_func(func: Callable[P, T], *args: P.args) -> T:
        return func(*args)
    

    This time Mypy says: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs". It looks like it wants me to also specify kwargs.

What is the correct way of doing it? Is there any technical difference between using TypeVarTuple and ParamSec in Callable?

like image 584
SorousH Bakhtiary Avatar asked Sep 17 '25 14:09

SorousH Bakhtiary


1 Answers

The only way to fully type-check something that only accepts positional-only arguments is with PEP 646, which mypy (as of the time of this answer) does not fully implement. However, you can manipulate typing.ParamSpec and typing.Protocol to make positional-only and keyword-only callable types which throw errors at a call site if someone tries to pass keyword and positional arguments, respectively.

The core idea is to form a union with Callable[P, R] with some Protocol CallbackProto such that:

  • Callable[P, R] unioned with CallbackProto::__call__(self, *args: typing.Any) -> R rejects all attempts to pass any keyword arguments;
  • Callable[P, R] unioned with CallbackProto::__call__(self, **kwargs: typing.Any) -> R rejects all attempts to pass any positional arguments.

In your case, let's say that this was the desired output:

P = ParamSpec("P")
R = TypeVar("R")

def call_func(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
    return func(*args)

>>> def function_(a: int, b: str) -> None:
...     ...
...
>>> call_func(function_, 1, "")  # OK
>>> call_func(function_, 1, b="")  # Unexpected keyword argument "b"

This can be done by making call_func reject all attempts to pass keyword arguments:

from __future__ import annotations

import typing as t

if t.TYPE_CHECKING:
    from typing_extensions import Never as UnionReturnTypePlaceholder
    import collections.abc as cx

P = t.ParamSpec("P")
R = t.TypeVar("R")
CallFuncT = t.TypeVar("CallFuncT", bound="CallFunc")

class PositionalOnlyCallable(t.Protocol):
    def __call__(self, *args: t.Any) -> UnionReturnTypePlaceholder:
        ...

class CallFunc(t.Protocol):
    def __call__(self, func: cx.Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
        ...

def asPositionalOnlyCallable(f: CallFuncT, /) -> CallFuncT | PositionalOnlyCallable:
    """
    No-op decorator which manipulates the typing signature. Unions
    a given callable's type with something which only accepts
    positional arguments.
    """
    return f

@asPositionalOnlyCallable
def call_func(func: cx.Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
    return func(*args)
>>> def function_(a: int, b: str) -> None:
...     ...
...
>>> call_func(function_, 1, "")  # OK
>>> call_func(function_, 1, b="")  # mypy: Unexpected keyword argument "b" for "__call__" of "PositionalOnlyCallable" [call-arg]
like image 86
dROOOze Avatar answered Sep 20 '25 04:09

dROOOze