Dynamic Python Functions with types.FunctionType

This article explains how to programmatically construct and execute Python functions at runtime using the built-in types.FunctionType constructor. Developers will learn the mechanics of compiling Python source into bytecode, binding the required global namespace, configuring optional parameters such as default arguments, and invoking the generated function.

Understanding types.FunctionType

In Python, user-defined functions are instances of the standard function type, which is accessible via the types module as types.FunctionType. While functions are usually defined using the def keyword or lambda expressions, types.FunctionType allows direct instantiation of a function object from an underlying code object.

The constructor signature is:

types.FunctionType(code, globals, name=None, argdefs=None, closure=None)

Step-by-Step Implementation

Creating a function dynamically involves generating a bytecode object and passing it to the constructor alongside an execution context.

1. Generating the Code Object

A code object can be created using Python's built-in compile() function. When compiling a function body intended for execution, the string must represent a valid expression or an execution block.

To create a callable function with parameters, wrap the logic in a standard def structure and extract its code object, or compile an expression directly:

import types

# Define the source code as a string
source_code = """
def dynamic_add(a, b):
    return a + b
"""

# Compile the source into a module-level code object
compiled_module = compile(source_code, filename="<dynamic>", mode="exec")

# Extract the inner code object representing the function body
func_code = [c for c in compiled_module.co_consts if isinstance(c, types.CodeType)][0]

2. Instantiating the Function

Once the code object is obtained, instantiate types.FunctionType and pass the required execution namespace:

# Create a namespace dictionary
namespace = {}

# Instantiate the function
dynamic_func = types.FunctionType(func_code, namespace, name="dynamic_add")

# Execute the function
result = dynamic_func(10, 25)
print(result)  # Output: 35

Adding Default Arguments and Metadata

To specify default arguments, pass a tuple to the argdefs parameter. Defaults are assigned to positional arguments from right to left:

# Provide a default value for 'b'
dynamic_func_with_defaults = types.FunctionType(
    func_code,
    namespace,
    name="dynamic_add_default",
    argdefs=(100,)  # b defaults to 100
)

print(dynamic_func_with_defaults(5))  # Output: 105

You can also assign attributes directly to the function instance after creation, such as __doc__ or __annotations__, to ensure standard introspection tools work properly.

When to Use types.FunctionType

Instantiating functions via types.FunctionType is common in advanced metaprogramming, such as:

Direct usage of types.FunctionType bypasses standard syntax checks and requires manual management of namespaces and closures, so code objects must be strictly validated before execution.