11

Consider the following code sample

def sum(a: int, b: int):
  return a + b

def wrap(*args, **kwargs):
  # delegate to sum
  return sum(*args, **kwargs)

The code works well except that type hint is lost. It's very common in Python to use *args, **kwargs to implement delegation pattern. It would be great to have a way to keep the type hint while using them, but I don't know if it is possible and how.

4
  • No. I am looking for a way that allow IDE or type checking tool that can derive the typing automatically.
    – link89
    Commented Dec 13, 2021 at 4:02
  • 1
    There are ways to make this work at run time. Functions can have a __signature__ attribute copied from a wrapped function, for example, and the inspect module will respect it even if the wrapper function is defined with *args, **kwargs. functools.wraps() copies the signature object appropriately. I don't know how widely this is supported in IDEs since it only happens at runtime; PyCharm didn't really "get it" when I used @functools.wraps(sum) on your wrap function.
    – kindall
    Commented Dec 13, 2021 at 5:00
  • See PEP 362 for more information on function signature objects.
    – kindall
    Commented Dec 13, 2021 at 5:02
  • Thanks @kindall It is good to know to have a solution working at the run time, it will make debugging much easier. I know that Python is so dynamic that maybe no solution to solve this issue in the typing system. I find that PEP 612 maybe a potential one but I didn't test it yet.
    – link89
    Commented Dec 13, 2021 at 10:37

1 Answer 1

6

See https://github.com/python/typing/issues/270 for a long discussion of this problem. You can achieve this by decorating wrap with an appropriately typed identity function:

F = TypeVar("F", bound=Callable)
def copy_signature(_: F) -> Callable[..., F]:
    return lambda f: f

def s(x: int, y: int) -> int:
    return x + y

@copy_signature(s)
def wrap(*args, **kwargs):
    s(*args, **kwargs)

reveal_type(wrap)  # Revealed type is "def (x: int, y: int) -> int"

As far as I know, the decorator is necessary - it is still not possible to do this using type hints alone, even with PEP612. Since it was already good practice to use the functools.wraps decorator in this situation (which copies the runtime type information), this is not such a loss - you could instead define

def wraps(f: F) -> Callable[..., F]:
    return functools.wraps(f) # type: ignore

and then both the runtime and static type information should be correct so long as you use this decorator. (Sadly the typeshed stubs for functools.wraps included with mypy aren't quite restrictive enough to get this working out of the box.)

PEP612 adds the ability to add/remove arguments in your wrapper (by combining ParamSpec with Concatenate), but it doesn't remove the need for some kind of higher-order function (like a decorator) to let the type system infer the signature of wrap from that of s.

3
  • I think that's the right way to do so and the IDE can also derive typing correclty.
    – link89
    Commented Dec 10, 2022 at 1:22
  • Great answer! That's only if it delegates all args to the original F though, right? Is it possible to wrap and extend one signature with additional args?
    – Mu Mind
    Commented Jun 27 at 16:36
  • 1
    @MuMind: You can use ParamSpec and Concatenate to prepend additional positional arguments, or TypeVarTuple to append positional arguments (though this loses the parameter names, and cannot be used on functions that have kwargs). As far as I know there's not yet a generic way to manipulate the **kwargs part of a callable signature. Commented Jun 29 at 1:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.