2

I'm trying to create a function dynamically. Here is an example:

import ast
import textwrap
from typing import Any, Callable, List, Union


def create_function(
    func_name: str,
    arg_types: List[Any],
):
    union_elts = [ast.Name(id=t.__name__, ctx=ast.Load()) for t in arg_types]
    vararg_annotation = ast.Subscript(
        value=ast.Name(id='Union', ctx=ast.Load()),
        slice=ast.Tuple(elts=union_elts, ctx=ast.Load()),
        ctx=ast.Load()
    )

    args_node = ast.arguments(
        posonlyargs=[],
        args=[],
        vararg=ast.arg(arg='args', annotation=vararg_annotation),
        kwonlyargs=[],
        kw_defaults=[],
        defaults=[]
    )

    body_code = f"""
    return ', '.join([str(t) for t in args])
    """

    function_def = ast.FunctionDef(
        name=func_name,
        args=args_node,
        returns=ast.Name(id=str.__name__, ctx=ast.Load()),
        body=ast.parse(textwrap.dedent(body_code)).body,
        decorator_list=[],
    )

    module = ast.Module(body=[function_def], type_ignores=[])
    ast.fix_missing_locations(module)
    code = compile(module, filename="<generated_code>", mode="exec")
    exec_scope = {}
    exec(code, {'Union': Union, **globals()}, exec_scope)

    return exec_scope[func_name]


test: Callable[
    [Union[str | int | float], ...],
    str
] = create_function(
    func_name='test',
    arg_types=[str, int, float],
)


print(test('1', 2, 3.0, '4'))

The function works fine and description says test: (str | int | float, Any) -> str. But I see Unexpected argument warning after a second argument (PyCharm Build #PY-251.23774.444, built on April 15, 2025). How can I fix this, or is it an issue with my code editor?

I tried to clear file system cache and VCS log caches / indexes - it didn't help.

warning

5
  • 1
    arg_types=[str, int, float] says there are only 3 arguments, you're calling it with 4. Commented Nov 12 at 20:02
  • @Barmar yes. but I specified type [Union[str | int | float], ...] and expected it will works like def test(*args: Union[int, float, str]) -> str:. What am I missing? Looks like the problem with ast.Subscript or arg_types Commented Nov 12 at 20:05
  • When you assign a specific value to the variable, and there's no code path that could change the value, the type checker uses its more specific type. Commented Nov 12 at 20:29
  • I think you'll see something similar with test: int | dict = 1; test[3] = 'abc' Even though you've declared that it can be a dictionary, the type checker knows that it's an integer. Commented Nov 12 at 20:30
  • @Barmar that's a type error either way - int | dict is still not subscriptable. Commented Nov 13 at 0:02

1 Answer 1

4

From the documentation:

Callable cannot express complex signatures such as functions that take a variadic number of arguments, overloaded functions, or functions that have keyword-only parameters. However, these signatures can be expressed by defining a Protocol class with a __call__() method

To get a proper annotation, you could do something like:

class CreatedFunction[T](typing.Protocol):
    def __call__(self, *args: T) -> str:
        pass

test: CreatedFunction[
    str | int | float
] = create_function(
    func_name='test',
    arg_types=[str, int, float],
)

The solution suggested in the comments by dROOOze does not do what you expect, it would generate a signature like def test(arg: str | int | float, *args: Any) -> str instead.

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

Comments

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.