getattr, settattr, and Compound Attributes in Maya Dependency Graph Nodes

28 Jan 2026

The purpose of this post is to give Maya Python developers an example of using getattr and setattr to set class attributes when writing a custom dependency graph plugin. This technique allows programmers to take advantage of Python language features with minimal changes to the standard Maya dependency graph node programming syntax laid out by Autodesk. This post includes:

I use this technique in several nodes that are part of my custom animation pipeline. In my pipeline, the node attribute names are defined by an external configuration. Taking advantage of the builtins allows easier implementation of object-oriented programming patterns. For instance, you could pass node classes to a builder method. Finally, using the builtins with a compound attribute allows a low-impact solution to creating attributes with unknown names.

GOAL: I will replicate my animation pipeline’s use case with a simple example node that

a maya node

My example node

SET UP: The first step of creating a new node is fussing over the unique hexacimal numeric ID to assign to it. If the node is internal to your organization, you can use any hexadecimal number 0 - 0x7fff. If you intend to distribute your node publicly, you can also request to reserve a block of nodes IDs with Autodesk. I was able to easily reserve a 64 ID block on the Autodesk Developer Network in Novemeber 2024.

For this example, I’ll use 0x00141A44. You can read more about the Maya Node TypeIDs here and review their Python 2.0 API reference here.

When starting a new node, I make a little skeleton with the essential node functions:

from maya.api import OpenMaya as om
maya_useNewAPI = True

class ExampleNode(om.MPxNode):
    id = om.MTypeId(0x00141A44)

    @staticmethod
    def creator():
        return ExampleNode()
    
    @staticmethod
    def initialize():
        return
    
    def compute(self, plug, datablock):
        return self
    
def initializePlugin(mobject):
	mplugin = om.MFnPlugin(mobject)
	try: 
		node_name = 'ExampleNode_py'
		node_id = ExampleNode.id
		mplugin.registerNode(node_name, node_id, ExampleNode.creator, ExampleNode.initialize)
	except:
		print("Failed to register node: %s" % node_name)
		raise
      
def uninitializePlugin(mobject):
	mplugin = om.MFnPlugin(mobject)

	try:
		node_name = 'ExampleNode_py'
		node_id = ExampleNode.id
		mplugin.deregisterNode(node_id)
	except:
		print("Failed to deregister node: %s" % node_name)

I also make a little script to load the node called load.py.

import maya.cmds as cmds
import platform

def load_plugin():
	if platform.system() == 'Windows':
		plugin_path = 'The path to your plugin if you are developing on Windows'
	else:
		plugin_path = 'The path to your plugin if you are developing on Unix'
	cmds.loadPlugin(plugin_path)

load_plugin()

INITIALIZE function: Example initialize functions will generally look something like this:

def initialize():
  n_attr = om.MFnNumericAttribute()
  ExampleNode.output = n_attr.create("output", "out", MFnNumericData::kFloat, 0.0)
  om.MPxNode.addAttribute(ExampleNode.output)

An update to this syntax using the builtins looks like this:

def initialize():
  n_attr = om.MFnNumericAttribute()
  op = n_attr.create("output", "out", MFnNumericData::kFloat, 0.0)
  setattr(ExampleNode,"output",op)
  om.MPxNode.addAttribute(getattr(ExampleNode,"output"),op)

Additionally, note that the attribute names are set as class attributes:

class ExampleNode(om.MPxNode):
    id = om.MTypeId(0x00141A44)
    output = None
    inputs = None
    called_out = None

Here is the initialize function in my final node, written to the specifications laid out in the ‘GOAL’ section:

def initialize():
    # The attribute Function Sets. Operate on the data using the Function Set!
    n_attr = om.MFnNumericAttribute()
    c_attr = om.MFnCompoundAttribute()
    t_attr = om.MFnTypedAttribute()

    # The attribute MObjects. The data itself. Not mine!
    output = n_attr.create('output', 'op', om.MFnNumericData.kFloat, defaultValue=1.0)
    setattr(ExampleNode, "output", output)
    inputs = c_attr.create('inputs', 'ip')
    setattr(ExampleNode, "inputs", inputs)
    value_names = t_attr.create("called_out", "co", om.MFnData.kString)
    setattr(ExampleNode, "called_out", value_names)

    om.MPxNode.addAttribute(getattr(ExampleNode, "output"))
    om.MPxNode.addAttribute(getattr(ExampleNode, "called_out"))

    for k,v in my_dict.items():
        inp = n_attr.create(k, v, om.MFnNumericData.kFloat, defaultValue=0.0)
        # I do not need to use addAttribute on children of compound attributes. addChild is sufficient
        c_attr.addChild(inp)

        # I add all the child attributes to the node class so I can access them in the compute function
        # Not strictly necessary -- could also iterate through compound plug children. But this solution is clean.
        setattr(ExampleNode,k,inp)
    
    # Notice how I don't add the compound attribute until I add all its children
    # This doesn't matter for the typed attributes and numeric attributes, but for compound attributes
    # called addChild on a compound attribute that has already been added will crash Maya when creating the node
    om.MPxNode.addAttribute(getattr(ExampleNode, "inputs"))

    om.MPxNode.attributeAffects(getattr(ExampleNode, "called_out"), getattr(ExampleNode, "output"))

    # Note how I set the compound attribute as the affecting attribute, not each child
    om.MPxNode.attributeAffects(getattr(ExampleNode, "inputs"), getattr(ExampleNode, "output"))
    return

COMPUTE function: Example compute functions will generally look something like this:

def compute(self, plug, datablock):
    # Recall that the compute function is called on each plug for the node
    if plug == ExampleNode.output:
        called_out = datablock.inputValue(ExampleNode.called_out).asString()
        # DO SOME OPERATIONS TO CALCULATE A VALUE FOR ExampleNode.output, store as a variable called output_value
        out_handle = datablock.outputValue(plug)
        out_handle.setFloat(output_value)
        out_handle.setClean()
    return self

An update to this syntax using the builtins looks like this:

def compute(self, plug, datablock):
    # Recall that the compute function is called on each plug for the node
    if plug == getattr(ExampleNode,"output"):
        called_out = datablock.inputValue(getattr(ExampleNode,"called_out")).asString()
        # DO SOME OPERATIONS TO CALCULATE A VALUE FOR ExampleNode.output, store as a variable called output_value
        out_handle = datablock.outputValue(plug)
        out_handle.setFloat(output_value)
        out_handle.setClean()
    return self

Here is the compute function in my final node, written to the specifications laid out in the ‘GOAL’ section:

def compute(self, plug, datablock):
    if plug == getattr(ExampleNode,"output"):
        # ExampleNode.output is the MObject itself. Not mine! The datablock allows me to access the value
        called_out = datablock.inputValue(getattr(ExampleNode,"calledOut")).asString()
        if called_out == '':
            return self
        called_out_attr_names = called_out.split(',')
        called_out_inputs = [getattr(ExampleNode,an) for an in called_out_attr_names]
        product = 1.0
        for inp in called_out_inputs:
            multiple = datablock.inputValue(inp).asFloat()
            product = product * multiple
        
        out_handle = datablock.outputValue(plug)
        out_handle.setFloat(product)
        out_handle.setClean()
    return self

FINAL CODE: Here is my completed node, written to the specifications in the ‘GOAL’ section:

from maya.api import OpenMaya as om
maya_useNewAPI = True

my_dict = {
  "attrA":"aa",
  "attrB":"ab",
  "attrC":"ac",
  "attrD":"ad",
  "attrE":"ae"
}

class ExampleNode(om.MPxNode):
    id = om.MTypeId(0x00141A44)
    output = None
    inputs = None
    called_out = None

    @staticmethod
    def creator():
        return ExampleNode()
    
    @staticmethod
    def initialize():
        # The attribute Function Sets. Operate on the data using the Function Set!
        n_attr = om.MFnNumericAttribute()
        c_attr = om.MFnCompoundAttribute()
        t_attr = om.MFnTypedAttribute()

        # The attribute MObjects. The data itself. Not mine!
        output = n_attr.create('output', 'op', om.MFnNumericData.kFloat, defaultValue=1.0)
        setattr(ExampleNode, "output", output)
        inputs = c_attr.create('inputs', 'ip')
        setattr(ExampleNode, "inputs", inputs)
        value_names = t_attr.create("called_out", "co", om.MFnData.kString)
        setattr(ExampleNode, "called_out", value_names)

        om.MPxNode.addAttribute(getattr(ExampleNode, "output"))
        om.MPxNode.addAttribute(getattr(ExampleNode, "called_out"))

        for k,v in my_dict.items():
            inp = n_attr.create(k, v, om.MFnNumericData.kFloat, defaultValue=0.0)
            # I do not need to use addAttribute on children of compound attributes. addChild is sufficient
            c_attr.addChild(inp)

            # I add all the child attributes to the node class so I can access them in the compute function
            # Not strictly necessary -- could also iterate through compound plug children. But this solution is clean.
            setattr(ExampleNode,k,inp)
        
        # Notice how I don't add the compound attribute until I add all its children
        # This doesn't matter for the typed attributes and numeric attributes, but for compound attributes
        # called addChild on a compound attribute that has already been added will crash Maya when creating the node
        om.MPxNode.addAttribute(getattr(ExampleNode, "inputs"))

        om.MPxNode.attributeAffects(getattr(ExampleNode, "called_out"), getattr(ExampleNode, "output"))

        # Note how I set the compound attribute as the affecting attribute, not each child
        om.MPxNode.attributeAffects(getattr(ExampleNode, "inputs"), getattr(ExampleNode, "output"))
        return
    
    def compute(self, plug, datablock):
        if plug == getattr(ExampleNode,"output"):
            # ExampleNode.output is the MObject itself. Not mine! The datablock allows me to access the value
            called_out = datablock.inputValue(getattr(ExampleNode,"called_out")).asString()
            if called_out == '':
                return self
            called_out_attr_names = called_out.split(',')
            called_out_inputs = [getattr(ExampleNode,an) for an in called_out_attr_names]
            product = 1.0
            for inp in called_out_inputs:
                multiple = datablock.inputValue(inp).asFloat()
                product = product * multiple
            
            out_handle = datablock.outputValue(plug)
            out_handle.setFloat(product)
            out_handle.setClean()
        return self
    
def initializePlugin(mobject):
    mplugin = om.MFnPlugin(mobject)
    try: 
        node_name = 'ExampleNode_py'
        node_id = ExampleNode.id
        mplugin.registerNode(node_name, node_id, ExampleNode.creator, ExampleNode.initialize)
    except:
        print("Failed to register node: %s" % node_name)
        raise
      
def uninitializePlugin(mobject):
    mplugin = om.MFnPlugin(mobject)

    try:
        node_name = 'ExampleNode_py'
        node_id = ExampleNode.id
        mplugin.deregisterNode(node_id)
    except:
        print("Failed to deregister node: %s" % node_name)

TEST LOOP: Here is a test loop for this node. You can write a script for the test loop. I prefer to test with the UI to get a sense of the artist workflow when using my node.

maya script editor

Maya Script Editor. For my development projects I use Chris Zurbrigg's Charcoal Editor instead

maya node editor

Maya Node Editor. You can certainly add the node to your scene using the createNode command instead.

a maya node

The node should appear like this in the Attribute Editor

a maya node

Notice how attrC and attrD values did not get included in the product

(Optional) TEST USING SCENE TIME: My favorite way to test a node with numeric inputs is to hook it up to the scene’s time node. This forces the compute function to re-evaluate each frame, meaning you can observe the node behavior by simply scrubbing the timeline.

finding the time1 node in the outliner

There is only one time1 node per scene

maya node editor add node button

The add node button

maya node editor connected nodes

If you want to hide the unconnected attributes after doing this process, hit the hamburger icon in the ExampleNode upper-right corner

updating based on time

ADDITIONAL RESOURCES: I hope experimenting with builtins in the context of Maya dependency graph nodes gives you a better grasp of the Maya Python API 2.0. Here are some of my favorite resources to learn more about the API: