Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Annotating a function with passthrough kwargs #1501

Unanswered
BrandonNav asked this question in Q&A
Discussion options

Hello,

I have a function which has no parameters other than kwargs which are to be passed onto a third party library call.

Here is a simplified example

def third_party_func(*, a: int, b: int):
 return 1.0
def run(**kwargs):
 return third_party_func(**kwargs)

I'd like to type kwargs so I can get both completion suggestions and better safety. I could create a TypedDict with all the parameters of the function call but that potentially breaks whenever the 3rd party is updated.

What I'd like to do is have a type checker consider the function signature of one function as the same as another.

Given I'm just passing the parameters through from one function to another, I considered pretending my function is akin to a decorator, so I could somehow use Paramspec to capture the kwargs of the function and pass them along

Something like the following:

P = ParamSpec("P")
R = TypeVar("R")
# 3rd party function I have no control over
def third_party_func(*, a: int, b: int):
 return 1.0
# Function wrappers to capture parameters and pass through
def wrapper(f: Callable[P, R]) -> Callable[..., Callable[P, R]]:
 def inner(e: Callable[P, R]) -> Callable[P, R]:
 def inside(*args: P.args, **kwargs: P.kwargs) -> R:
 return e(*args, **kwargs)
 return inside
 return inner
# Capture parameters of third_party_func
wrapped = wrapper(third_party_func)
# Decorate run function with wrapped
@wrapped
def run(**kwargs: Any):
 return third_party_func(**kwargs)
reveal_type(wrapped)
# mypy: Revealed type is "def (*Any, **Any) -> def (*, a: builtins.int, b: builtins.int) -> Any"
# pyright: Type of "wrapped" is "(...) -> ((*, a: int, b: int) -> float)"
reveal_type(run)
# mypy: Revealed type is "def (*, a: builtins.int, b: builtins.int) -> Any"
# pyright: Type of "wrapped_run" is "(*, a: int, b: int) -> float"

This "works" in so far as I can get completions (in vscode) and if I purely rely on my wrapped_run I should get warnings. But kwargs is still untyped (it be a dict[str, Any]) and I feel this isn't the intention of Paramspec and I'm risking this breaking in the future. Also, the fact mypy can't infer the return type (even with --new-type-inference) makes me think I'm on the wrong track.

I also thought over if I could use Unpack but you can't unpack P.kwargs nor can I find a way to statically create a TypedDict from a function signature.

Is there another way to accomplish this?

You must be logged in to vote

Replies: 1 comment 4 replies

Comment options

Your approach looks OK to me. If you really want to avoid any potential breaking changes, you'd need to support both *args and **kwargs in your run method because third_party_func could theoretically be modified in the future to accept positional arguments.

The reason you're not seeing a return type in mypy is that it never infers return types for functions. If you add an explicit return type annotation to third_party_func (e.g. -> float), then it will work fine with mypy.

Here are a couple of other approaches you might consider:

  1. Use functools.wraps. This works fine in pyright, but for some reason doesn't work with mypy.
from functools import wraps
@wraps(third_party_func)
def run(**kwargs: Any):
 return third_party_func(**kwargs)
  1. Simply create an alias for the third_party_func function.
run = third_party_func
  1. Manually duplicate the signature of third_party_func in your run function. If the signature of third_party_func changes in the future in an incompatible manner, your type checker will notify you of the break.
def run(*, a: int, b: int) -> float:
 return third_party_func(a=a, b=b)
You must be logged in to vote
4 replies
Comment options

Thanks for your response @erictraut

wraps does simplify things so thanks for pointing that out.
One of the reasons I was looking for an alternative was to see if I could expand the method so the kwargs parameter could be manipulated safely

e.g.:

from functools import wraps
@wraps(third_party_func)
def run(**kwargs: Any):
 # Preamble where we manipulate kwargs
 c = kwargs["c"] # Would be nice if this could error 
 return third_party_func(**kwargs)

I think typing kwargs the way I want isn't possible as pyright will say kwargs is Unknown with inference. My hope with using Paramspec was the inference engine would see that that type of the kwargs came from decorator. As I can't have a type Kwargs = TypedDict.from[KwargsOf[third_party_func]]] I'll try your approach 3 for my expanded need

Comment options

Use functools.wraps. This works fine in pyright, but for some reason doesn't work with mypy.

Mypy's vendored typeshed has reverted the use of ParamSpec for functools.wraps. There's some discussion here
python/mypy@7d987a1#commitcomment-127920750

Comment options

@Hnasar FYI the revert was reverted in python/mypy@a00fcba.

However it remains pretty difficult to do what this question is asking. It's even worse if your function has many arguments with default values that you don't wish to repeat. As far as I can tell functools.wraps ends up discarding all type information.

Comment options

Another use case here is typing a dictionary for splatting. https://github.com/iloveitaly/python-starter-template/blob/7be8521e5e224cf1101159c697423d69560d5282/app/server.py#L21-L32

Too bad this isn't supported yet!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /