Search code examples
pythonpython-3.xabstract-syntax-treekeyword-argument

Using ast to create a function with a keyword-only argument that has a default value


I'm trying to use ast to dynamically create a function with a keyword-only argument that has a default value. However, the resulting function still requires the argument and will raise TypeError if it's not passed.

This is the code creating the function f:

import ast
import types

module_ast = ast.Module(
    body=[
        ast.FunctionDef(
            name='f',
            args=ast.arguments(
                args=[],
                vararg=None,
                kwarg=None,
                defaults=[],
                kwonlyargs=[
                    ast.arg(
                        arg='x',
                        lineno=1,
                        col_offset=0,
                    ),
                ],
                kw_defaults=[
                    ast.Num(n=42, lineno=1, col_offset=0),
                ],
                posonlyargs=[],
            ),
            body=[ast.Return(
                value=ast.Name(id='x', ctx=ast.Load(), lineno=1, col_offset=0),
                lineno=1,
                col_offset=0,
            )],
            decorator_list=[],
            lineno=1,
            col_offset=0,
        )
    ],
    type_ignores=[],
)

module_code = compile(module_ast, '<ast>', 'exec')

# This is the part that I'm suspicious of
f_code = next(c for c in module_code.co_consts if isinstance(c, types.CodeType))

f = types.FunctionType(
    f_code,
    {}
)

If I print(ast.unparse(module_ast)), I get what I expect:

def f(*, x=42):
    return x

Calling f(x=100) returns 100 as expected, but calling f() produces:

TypeError: f() missing 1 required keyword-only argument: 'x'

I suspect that the problem is in way the I'm turning the AST into a function. I saw the approach in another question here (which I unfortunately do not have a link to). It looks a bit dodgy, but I'm not sure how else to do it.


Solution

  • A function's default argument values aren't part of the function's code object. They can't be, because the defaults are created at function definition time, not bytecode compilation time. Default argument values are stored in __defaults__ for non-keyword-only arguments and __kwdefaults__ for keyword-only arguments. When you extract f_code from module_code, you're not getting any information about defaults.

    Execute the function definition, then retrieve the actual function object:

    namespace = {}
    exec(module_code, namespace)
    function = namespace['f']