How to add Inputs and Outputs to Group Node using Python

So I’m trying to write code that creates a custom set of Nodes for the ShaderEditor.
Code:

import bpy

def create_custom_node_group(self, context):

    node_tree = bpy.data.node_groups.new('custom group', 'ShaderNodeTree')
    node_tree.inputs.new('NodeSocketColor', 'Color')
    node_tree.outputs.new('NodeSocketColor', 'Output')

    group_in = node_tree.nodes.new('NodeGroupInput')
    group_in.location = (-200, 0)

    group_out = node_tree.nodes.new('NodeGroupOutput')
    group_out.location = (200, 0)
    
    mix_node = node_tree.nodes.new(type= 'ShaderNodeMixRGB')
    mix_node.location = (0, 0)
    mix_node.inputs[0].default_vaule = 0.6
    mix_node.inputs[7].default_vaule = (0, 1, 0, 1)

    link = node_tree.links.new

    link(group_in.outputs[0], mix_node.inputs[6])
    link(mix_node.outputs[0], group_out.inputs[0])

    return node_tree

class NODE_OT_test(bpy.types.Operator):
     bl_label = "Test"
     bl_idname = "node.test_operator"

     def execute(self, context):
          group = create_custom_node_group(context)

          context.active_object.active_material.use_nodes = True

          node = context.active_object.active_material.node_tree.nodes.new('ShaderNodeGroup')
          node.node_tree = bpy.data.node_groups[node_name]
          node.use_custom_color = True
          node.color = (0.5, 0.4, 0.7)

          return {'FINISHED'}

The thing is, when I try to run the script, I get the following error:
AttributeError: 'ShaderNodeTree' object has no attribute 'inputs'

I would love to get help.

if bl<4.0

def create_custom_node_group(self, context):
    node_tree = bpy.data.node_groups.new('custom group', 'ShaderNodeTree')

  ......
    group_in = node_tree.nodes.new('NodeGroupInput')
    group_out = node_tree.nodes.new('NodeGroupOutput')

    group_in.location = (-200, 0)
    group_out.location = (200, 0)


    input_socket = group_in.inputs.new('NodeSocketColor', 'Color')
    output_socket = group_out.outputs.new('NodeSocketColor', 'Output')

    node_tree.links.new(group_in.outputs["Color"], mix_node.inputs[1])
    node_tree.links.new(mix_node.outputs[0], group_out.inputs["Output"])

    return {'FINISHED'}

Hi GoldenDragon,

in Blender 4.0 the inputs and outputs got removed from the NodeTree
Now you have to add them to the NodeTree interface. Here’s a modified version of your function:

Code
def create_custom_node_group():
    
    # Create new node group
    group = bpy.data.node_groups.new('custom group', 'ShaderNodeTree')
    
    # Define interface
    color_input = group.interface.new_socket(
        name="Color",
        description="Color input",
        in_out='INPUT',
        socket_type='NodeSocketColor'
    )
    color_input.default_value = (1.0, 1.0, 1.0, 1.0)
    
    color_output = group.interface.new_socket(
        name="Color",
        description="Color output",
        in_out='OUTPUT',
        socket_type='NodeSocketColor'
    )
    color_output.default_value = (0.0, 0.0, 0.0, 1.0)
    
    # Create nodes
    nodes = group.nodes
    group_in = nodes.new(type='NodeGroupInput')
    group_in.location = (-200, 0)

    group_out = nodes.new(type='NodeGroupOutput')
    group_out.location = (200, 0)
    
    mix_node = nodes.new(type='ShaderNodeMix')
    mix_node.location = (0, 0)
    mix_node.data_type = 'RGBA'
    mix_node.inputs[0].default_value = 0.6
    mix_node.inputs[7].default_value = (0.0, 1.0, 0.0, 1.0)

    # Create links
    links = group.links
    links.new(group_in.outputs[0], mix_node.inputs[6])
    links.new(mix_node.outputs[2], group_out.inputs[0])

    return group

Do you know how to set the subtype? So say I create a nodesocketfloat. By default the subtype is not set, this causes issues when I try to connect say factor. This only runs from min 0 to max 1.0. but now min is inf and max is inf

I’ve veen searching Google for some hours, but it seems documentation and api still lacks the info

The socket objects have a property called “.subtype”. For example:

fac_input = group.interface.new_socket(
    name="Fac",
    description="Factor",
    in_out='INPUT',
    socket_type='NodeSocketFloat'
)

fac_input.subtype = 'FACTOR'
fac_input.default_value = 0.5
fac_input.min_value = 0.0
fac_input.max_value = 1.0

You could also activate the “Python Tooltips” in the preferences for some help:


This way, you’ll get an information about which property/operator to call in python:
image

I don’t know about subtype specifically but if you can recreate this somehow from UI, then you can export your node graph using https://extensions.blender.org/add-ons/node-to-python/ to Python and then investigate the code for solutions.

I know that dev part, ive used it for years. But seeing the example code, it would not matter since its calling an operator. The API, most times, is not clear how to actually use the code.

After 2-4 hours of googling i did find exactly what you showed here, also thanks for that. Could be useful for others.

Now im still looking for api to add it to sub-panels. But i believe this has not been exposed to the API yet. I did find some code in the node.py in bl_operators folder. But it uses a specific context.

Ow that looks handy! Thanks for showing that!

@Zebrahead showed the solution to my seearch

Try it like this (using the group and fac_input from above):

# Create a new panel
panel = group.interface.new_panel(name="My panel")

# Move socket to the panel
group.interface.move_to_parent(
    item=fac_input,
    parent=panel,
    to_position=0
)

# Or assign a parent when creating a new socket
col_input = group.interface.new_socket(
    name="Color",
    in_out='INPUT',
    socket_type='NodeSocketColor',
    parent=panel
)

Hope that helps!

Great! Ill try this

I also found an Addon called NodesToPython, which showed me another approach

import bpy, mathutils

# Generate unique scene name
base_name = "LG_View Layer"
end_name = base_name
if bpy.data.scenes.get(end_name) != None:
    i = 1
    end_name = base_name + f".{i:03d}"
    while bpy.data.scenes.get(end_name) != None:
        end_name = base_name + f".{i:03d}"
        i += 1

scene = bpy.context.window.scene.copy()

scene.name = end_name
scene.use_fake_user = True
bpy.context.window.scene = scene
#initialize LG_View Layer node group
def lg_view_layer_node_group():
    lg_view_layer = scene.node_tree
    #start with a clean node tree
    for node in lg_view_layer.nodes:
        lg_view_layer.nodes.remove(node)
    lg_view_layer.color_tag = 'NONE'
    lg_view_layer.description = ""
    lg_view_layer.default_group_node_width = 140
    

    # # Create NodeGroup
    # nt = context.scene.node_tree
    # viewlayer_node = nt.nodes.active
    # viewlayer_name = viewlayer_node.layer


    # create group node
    group_node = lg_view_layer.nodes.new(type='CompositorNodeGroup')
    group_node.location = [0,0] #(viewlayer_node.location[0] + 300, viewlayer_node.location[1])
    group_nt = bpy.data.node_groups.new(name='LG_' + base_name, type='CompositorNodeTree')
    group_node.node_tree = group_nt

    lg_view_layer = group_nt
    #lg_view_layer interface
    #Socket Outout
    outout_socket = lg_view_layer.interface.new_socket(name = "Outout", in_out='OUTPUT', socket_type = 'NodeSocketColor')
    outout_socket.default_value = (0.0, 0.0, 0.0, 1.0)
    outout_socket.attribute_domain = 'POINT'

    #Panel Area
    area_panel = lg_view_layer.interface.new_panel("Area")
    area_panel.description = "Point"
    #Socket Factor
    factor_socket = lg_view_layer.interface.new_socket(name = "Factor", in_out='INPUT', socket_type = 'NodeSocketFloat', parent = area_panel)
    factor_socket.default_value = 0.0
    factor_socket.min_value = 0.0
    factor_socket.max_value = 1.0
    factor_socket.subtype = 'FACTOR'
    factor_socket.attribute_domain = 'POINT'

    #Socket Color
    color_socket = lg_view_layer.interface.new_socket(name = "Color", in_out='INPUT', socket_type = 'NodeSocketColor', parent = area_panel)
    color_socket.default_value = (0.0, 0.0, 0.0, 0.0)
    color_socket.attribute_domain = 'POINT'

    #Socket Area Lights
    area_lights_socket = lg_view_layer.interface.new_socket(name = "Area Lights", in_out='INPUT', socket_type = 'NodeSocketColor', parent = area_panel)
    area_lights_socket.default_value = (0.0, 0.0, 0.0, 1.0)
    area_lights_socket.attribute_domain = 'POINT'