# How to copy nodes and their links from one node tree/material to another - in python?

I'm trying to copy nodes from one node tree/material to another, in python.

As far as I can tell there is no good way to do this.

I have even tried context switching and then either try to use

bpy.ops.node.clipboard_copy() or bpy.ops.node.group_make() on the selected nodes.

But both will fail due to wrong context, even though I have successfully changed the area.type to "NODE_EDITOR" or the area.ui_type to "ShaderNodeTree".

So, I'm at a loss here. I don't want to have to figure out the entire tree structure and re-create all individual nodes. I can select the nodes I need easily though. I would also be fine with somehow merging both trees and removing what I don't need.

Any help with this would be really appreciated. Blender 2.80. Just getting the node ops to work after a context switch to the node editor, would suffice already.

To clarify: I'm running my tool from VIEW_3D in object mode. I'm switching the context to the node editor like so:

def change_context(string):
area = bpy.context.area
old_type = area.type
area.type = string
return old_type


This works, in that it switches the 3d view to the node editor, but following this with calling bpy.ops.node.group_make() or bpy.ops.node.copy() will fail:

RuntimeError: Operator bpy.ops.node.group_make.poll() failed, context is incorrect


Calling the node ops from the search bar in proper, manually changed context works. So I suspect my context switching is incomplete?

1. running group_make() from node editor, works fine
2. switching area to node editor and running it, fails
• Do you really need to switch the context? Where do you call it? Nov 29 '18 at 18:28
• @jaroslav-jerryno-novotny I'm aware of these things, as I said, I don't want to recreate arbitarry node trees, node by node. Nov 29 '18 at 18:31
• Ok, then I don't have a solution.. Nov 29 '18 at 18:32
• Did you ever find a solution for this? I had the same approach before I found this and ran into the same issue. I stumbled upon this simple script which calls the poll function for all area types. I tried this with the 2.8 types for group_make and none of the types returned True. Apr 22 '20 at 10:40
• With a little more testing I found the C code for the operators poll function and it seems like it is testing if the attribute space.edit_tree is not None. When you set the areas type to NODE_EDITOR it seems like that attribute is None while the operator is still running. Afterwards it's set though. I'll do some more testing to see if I can figure out how to set it. Apr 22 '20 at 16:50

After some experimenting, I couldn't find any way of doing this with bpy.data or with the operator for making a group, so I decided to write my own: This operator takes in the name of the node group that should be created and then moves all selected nodes from the actives object active material in a node group with that name. It also copies all the links between the nodes and the property values. It doesn't just copy the input values but also dropdowns and similar options.

Here's the code:

class NODE_OT_MakeGroup(bpy.types.Operator):
bl_idname = "node.group_make_no_context"
bl_label = "Make Context Free Group"
bl_description = "Make group from selected nodes without context"
bl_options = { "REGISTER", "UNDO" }

name: bpy.props.StringProperty( default="NodeGroup" )

@classmethod
def poll( cls, context ):
"""run operator only if an active object with an active material exists"""

if context.active_object:
if context.active_object.active_material:
if context.active_object.active_material.use_nodes:
return True
return False

def get_selected_nodes(self,context):
"""returns a list of the selected nodes from the active objects active material"""

nodes = []
for node in context.active_object.active_material.node_tree.nodes:
if node.select:
nodes.append(node)
return nodes

def copy_attributes(self, attributes, old_prop, new_prop):
"""copies the list of attributes from the old to the new prop if the attribute exists"""

#check if the attribute exists and copy it
for attr in attributes:
if hasattr( new_prop, attr ):
setattr( new_prop, attr, getattr( old_prop, attr ) )

def get_node_attributes(self, node):
"""returns a list of all propertie identifiers if they shoulnd't be ignored"""

#all attributes that shouldn't be copied
ignore_attributes = ( "rna_type", "type", "dimensions", "inputs", "outputs", "internal_links", "select")

attributes = []
for attr in node.bl_rna.properties:
#check if the attribute should be copied and add it to the list of attributes to copy
if not attr.identifier in ignore_attributes and not attr.identifier.split("_")[0] == "bl":
attributes.append(attr.identifier)

return attributes

def copy_nodes(self, nodes, group):
"""copies all nodes from the given list into the group with their attributes"""

#the attributes that should be copied for every link
input_attributes = ( "default_value", "name" )
output_attributes = ( "default_value", "name" )

for node in nodes:
#create a new node in the group and find and copy its attributes
new_node = group.nodes.new( node.bl_idname )
node_attributes = self.get_node_attributes( node )
self.copy_attributes( node_attributes, node, new_node )

#copy the attributes for all inputs
for i, inp in enumerate(node.inputs):
self.copy_attributes( input_attributes, inp, new_node.inputs[i] )

#copy the attributes for all outputs
for i, out in enumerate(node.outputs):
self.copy_attributes( output_attributes, out, new_node.outputs[i] )

"""copies all links between the nodes in the list to the nodes in the group"""

for node in nodes:
#find the corresponding node in the created group
new_node = group.nodes[ node.name ]

#enumerate over every link in the nodes inputs
for i, inp in enumerate( node.inputs ):
#find the connected node for the link in the group
#connect the group nodes

"""adds the group input and output node and positions them correctly"""

group_input = group.nodes.new("NodeGroupInput")
group_output = group.nodes.new("NodeGroupOutput")

#if there are any nodes in the group, find the mini and maxi x position of all nodes and position the group nodes
if len(group.nodes) > 0:
min_pos = 9999999
max_pos = -9999999

for node in group.nodes:
if node.location[0] < min_pos:
min_pos = node.location[0]
elif node.location[0] + node.width > max_pos:
max_pos = node.location[0]

group_input.location = (min_pos - 250, 0)
group_output.location = (max_pos + 250, 0)

def execute( self, context ):
"""creates the node group, copies all attributes and links and adds the group input and output"""

#create new node group
group = bpy.data.node_groups.new( name=self.name, type="ShaderNodeTree" )
#get the list of all selected nodes from the active objects active material
nodes = self.get_selected_nodes(context)

#copy all nodes from the list to the created group with all their attributes
self.copy_nodes( nodes, group )
#copy the links between the nodes to the created groups nodes

#add the group input and output node to the created group

return { "FINISHED" }


I'm sure there are some improvements that could be done to this and of course, it's not a perfect solution because that operator basically already exists, but I think it's the best option while these are not working. I tried it with 2.83 and it seems to work fine for all cases I tested.

EDIT: Here's a version that was better suited for my personal case. It has an additional parameter to specify a specific material. If it doesn't get a value for that parameter it defaults to the active objects active material. Otherwise, it's the same as the one above:

class NODE_OT_MakeGroup(bpy.types.Operator):
bl_idname = "node.group_make_no_context"
bl_label = "Make Context Free Group"
bl_description = "Make group from selected nodes without context"
bl_options = { "REGISTER", "UNDO" }

name: bpy.props.StringProperty( default="NodeGroup" )
material: bpy.props.StringProperty( default="")

def get_selected_nodes(self,context):
"""returns a list of the selected nodes from the material"""

nodes = []
for node in bpy.data.materials[self.material].node_tree.nodes:
if node.select:
nodes.append(node)
return nodes

def copy_attributes(self, attributes, old_prop, new_prop):
"""copies the list of attributes from the old to the new prop if the attribute exists"""

#check if the attribute exists and copy it
for attr in attributes:
if hasattr( new_prop, attr ):
setattr( new_prop, attr, getattr( old_prop, attr ) )

def get_node_attributes(self, node):
"""returns a list of all propertie identifiers if they shoulnd't be ignored"""

#all attributes that shouldn't be copied
ignore_attributes = ( "rna_type", "type", "dimensions", "inputs", "outputs", "internal_links", "select")

attributes = []
for attr in node.bl_rna.properties:
#check if the attribute should be copied and add it to the list of attributes to copy
if not attr.identifier in ignore_attributes and not attr.identifier.split("_")[0] == "bl":
attributes.append(attr.identifier)

return attributes

def copy_nodes(self, nodes, group):
"""copies all nodes from the given list into the group with their attributes"""

#the attributes that should be copied for every link
input_attributes = ( "default_value", "name" )
output_attributes = ( "default_value", "name" )

for node in nodes:
#create a new node in the group and find and copy its attributes
new_node = group.nodes.new( node.bl_idname )
node_attributes = self.get_node_attributes( node )
self.copy_attributes( node_attributes, node, new_node )

#copy the attributes for all inputs
for i, inp in enumerate(node.inputs):
self.copy_attributes( input_attributes, inp, new_node.inputs[i] )

#copy the attributes for all outputs
for i, out in enumerate(node.outputs):
self.copy_attributes( output_attributes, out, new_node.outputs[i] )

"""copies all links between the nodes in the list to the nodes in the group"""

for node in nodes:
#find the corresponding node in the created group
new_node = group.nodes[ node.name ]

#enumerate over every link in the nodes inputs
for i, inp in enumerate( node.inputs ):
#find the connected node for the link in the group
#connect the group nodes

"""adds the group input and output node and positions them correctly"""

group_input = group.nodes.new("NodeGroupInput")
group_output = group.nodes.new("NodeGroupOutput")

#if there are any nodes in the group, find the min and maxi x position of all nodes and position the group nodes
if len(group.nodes) > 0:
min_pos = 9999999
max_pos = -9999999

for node in group.nodes:
if node.location[0] < min_pos:
min_pos = node.location[0]
elif node.location[0] + node.width > max_pos:
max_pos = node.location[0]

group_input.location = (min_pos - 250, 0)
group_output.location = (max_pos + 250, 0)

def execute( self, context ):
"""creates the node group, copies all attributes and links and adds the group input and output"""

#set material if not given
if not self.material:
self.material = bpy.context.active_object.active_material.name

#create new node group
group = bpy.data.node_groups.new( name=self.name, type="ShaderNodeTree" )
#get the list of all selected nodes from the active objects active material
nodes = self.get_selected_nodes(context)

#copy all nodes from the list to the created group with all their attributes
self.copy_nodes( nodes, group )
#copy the links between the nodes to the created groups nodes

#add the group input and output node to the created group

return { "FINISHED" }


You can see here, the key is:bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1)

area.ui_type = 'ShaderNodeTree'
if area.spaces.active.node_tree != you_node_tree: