7

I am working within a Python web framework that uses Python 3 type annotations for validation and dependency injection.

So I am looking for a way to generate functions with type annotations from a parameters given to the generating function:

def gen_fn(args: Dict[str, Any]) -> Callable:
    def new_fn(???):
        pass
    return new_fn

so that

inspect.signature(gen_fn({'a': int}))

will return

<Signature (a:int)>

Is there something I cam put instead of the ??? that will do the thing I need.

I also looked at Signature.replace() in the inspect module, but did not find a way to attach the new signature to a new or existing function.

I am hesitant to use ast because:

The abstract syntax itself might change with each Python release

So my question is: What (if any) is a reasonable way to generate a function with Python 3 type annotation based on a dict passed to the generating function?


Edit: while @Aran-Fey's solution answer my question correctly, it appears that my assumption was wrong. Changing the signature doesn't allow calling the new_fn using the new signature. That is gen_fn({'a': int})(a=42) raises a TypeError: ... `got an unexpected keyword argument 'a'.

2
  • 1
    I guess one way to achieve that is to build a string and eval() the whole string to get the function definition. But I don't think that's the best way to do it. Commented May 17, 2018 at 6:40
  • MonkeyType can generate type annotations for some functions, but it requires the types to be checked at run-time. Commented Dec 3, 2020 at 23:14

1 Answer 1

12

Instead of creating a function with annotations, it's easier to create a function and then set the annotations manually.

  • inspect.signature looks for the existence of a __signature__ attribute before it looks at the function's actual signature, so we can craft an appropriate inspect.Signature object and assign it there:

    params = [inspect.Parameter(param,
                                inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                annotation=type_)
                            for param, type_ in args.items()]
    new_fn.__signature__ = inspect.Signature(params)
    
  • typing.get_type_hints does not respect __signature__, so we should update the __annotations__ attribute as well:

    new_fn.__annotations__ = args
    

Putting them both together:

def gen_fn(args: Dict[str, Any]) -> Callable:
    def new_fn():
        pass

    params = [inspect.Parameter(param,
                                inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                annotation=type_)
                            for param, type_ in args.items()]
    new_fn.__signature__ = inspect.Signature(params)
    new_fn.__annotations__ = args

    return new_fn

print(inspect.signature(gen_fn({'a': int})))  # (a:int)
print(get_type_hints(gen_fn({'a': int})))  # {'a': <class 'int'>}

Note that this doesn't make your function callable with these arguments; all of this is just smoke and mirrors that makes the function look like it has those parameters and annotations. Implementing the function is a separate issue.

You can define the function with varargs to aggregate all the arguments into a tuple and a dict:

def new_fn(*args, **kwargs):
    ...

But that still leaves you with the problem of implementing the function body. You haven't said what the function should do when it's called, so I can't help you with that. You can look at this question for some pointers.

Sign up to request clarification or add additional context in comments.

3 Comments

This seems to be correct but insufficient for my needs. See Edit to the OP.
@ChenLevy Yes, implementing the function is a separate issue. You can make the function accept arbitrary arguments if you define it as def new_fn(*args, **kwargs):, but you still have to implement the function's body. You haven't specified what the function is supposed to do when it's called, so I can't give you much advice. Depending on the circumstances, you may even have to resort the writing code at runtime and then execing it.
You made my day, sir!

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.