Maya Python API - OpenMaya 2.0

The Maya Python API gives you direct access to Maya's internal C++ architecture from Python. It's significantly faster than maya.cmds or PyMEL for data-heavy operations, and it's the only way to create custom dependency graph nodes, custom commands, and custom deformers through Python.

OpenMaya vs OpenMaya2 (API 1.0 vs 2.0)

Maya ships with two versions of the Python API:

  • API 1.0 - import maya.OpenMaya as om - The original binding. Uses SWIG wrappers around C++ classes. Requires MScriptUtil for passing pointers, which is awkward and error-prone.
  • API 2.0 - import maya.api.OpenMaya as om2 - The modern rewrite. Pythonic interface, no MScriptUtil, returns native Python types (lists, tuples). Roughly 2x faster than API 1.0.
Tip

Always use API 2.0 (maya.api.OpenMaya) for new code. API 1.0 is only needed for legacy plugins or rare features not yet ported to 2.0. All examples in this guide use API 2.0.

When to Use the API vs PyMEL/cmds

The API is more complex than PyMEL - so when is the complexity worth it?

  • Use PyMEL/cmds when building UI tools, simple scene operations, batch processing with moderate node counts, and any tool where development speed matters more than runtime performance.
  • Use the API when you need to process mesh data (vertices, UVs, normals) on thousands of objects, create custom dependency graph nodes, write custom commands that integrate with Maya's undo system, build deformers or manipulators, or when profiling shows PyMEL is a bottleneck.
Note

A common pattern is to use PyMEL for tool UI and high-level logic, and drop into the API for the performance-critical inner loop. You don't have to choose one or the other - they can coexist in the same tool.

MSelectionList and Working with MObjects

Everything in the API starts with MObject - an opaque handle to a Maya node. You get MObjects through MSelectionList, which is the API's way of resolving node names to internal references.

Python
import maya.api.OpenMaya as om2

# Create a selection list and add nodes by name
sel = om2.MSelectionList()
sel.add('pCube1')
sel.add('pSphere1')

# Get the MObject for the first item
mob = sel.getDependNode(0)
print(f'API type: {mob.apiTypeStr}')  # e.g. 'kTransform'

# Get the DAG path (for transforms and shapes)
dag_path = sel.getDagPath(0)
print(f'Full path: {dag_path.fullPathName()}')
print(f'Partial path: {dag_path.partialPathName()}')

# Extend the DAG path to the shape node
dag_path.extendToShape()
print(f'Shape path: {dag_path.fullPathName()}')
Python
import maya.api.OpenMaya as om2

def get_mobject(node_name):
    """Resolve a node name to an MObject."""
    sel = om2.MSelectionList()
    sel.add(node_name)
    return sel.getDependNode(0)

def get_dag_path(node_name):
    """Resolve a node name to an MDagPath."""
    sel = om2.MSelectionList()
    sel.add(node_name)
    return sel.getDagPath(0)

# Use MFnDependencyNode to query node info
mob = get_mobject('pCube1')
fn_node = om2.MFnDependencyNode(mob)
print(f'Node name: {fn_node.name()}')
print(f'Type name: {fn_node.typeName}')
print(f'Is shared: {fn_node.isShared}')

# Access attributes through the function set
translate_x_plug = fn_node.findPlug('translateX', False)
print(f'translateX = {translate_x_plug.asFloat()}')
Warning

MObject references can become invalid if the underlying node is deleted. Always check mob.isNull() before using a stored MObject, especially in callbacks or deferred operations.

Accessing Mesh Data

One of the most powerful uses of the API is direct mesh data access. MFnMesh lets you read and write vertex positions, normals, UVs, and face topology without the overhead of cmds.xform per-vertex calls.

Python
import maya.api.OpenMaya as om2

def get_mesh_data(mesh_name):
    """Extract vertex, face, and UV data from a mesh using the API."""
    # Get the DAG path to the shape
    sel = om2.MSelectionList()
    sel.add(mesh_name)
    dag_path = sel.getDagPath(0)
    dag_path.extendToShape()

    # Create the mesh function set
    fn_mesh = om2.MFnMesh(dag_path)

    # Get all vertex positions in world space
    points = fn_mesh.getPoints(om2.MSpace.kWorld)
    print(f'Vertex count: {len(points)}')
    for i, pt in enumerate(points[:5]):  # first 5 verts
        print(f'  vtx[{i}]: ({pt.x:.3f}, {pt.y:.3f}, {pt.z:.3f})')

    # Get face-vertex counts and indices
    face_counts, face_connects = fn_mesh.getVertices()
    print(f'Face count: {len(face_counts)}')

    # Get normals
    normals = fn_mesh.getNormals()
    print(f'Normal count: {len(normals)}')

    # Get UV data
    uv_set_names = fn_mesh.getUVSetNames()
    for uv_set in uv_set_names:
        us, vs = fn_mesh.getUVs(uv_set)
        print(f'UV set "{uv_set}": {len(us)} UVs')

    return {
        'points': points,
        'face_counts': face_counts,
        'face_connects': face_connects,
        'normals': normals,
    }

# Usage:
# data = get_mesh_data('pCube1')

Modifying Vertex Positions

Python
import maya.api.OpenMaya as om2
import math

def apply_sine_deformation(mesh_name, amplitude=0.5, frequency=2.0):
    """Apply a sine wave deformation along the Y axis based on X position."""
    sel = om2.MSelectionList()
    sel.add(mesh_name)
    dag_path = sel.getDagPath(0)
    dag_path.extendToShape()

    fn_mesh = om2.MFnMesh(dag_path)
    points = fn_mesh.getPoints(om2.MSpace.kObject)

    # Modify points with sine wave
    new_points = om2.MPointArray()
    for pt in points:
        offset = amplitude * math.sin(pt.x * frequency)
        new_points.append(om2.MPoint(pt.x, pt.y + offset, pt.z))

    # Set all points at once (much faster than per-vertex cmds)
    fn_mesh.setPoints(new_points, om2.MSpace.kObject)
    fn_mesh.updateSurface()

# Usage: create a high-res plane, then deform it
# import maya.cmds as cmds
# cmds.polyPlane(w=10, h=10, sx=50, sy=50, name='wave_plane')
# apply_sine_deformation('wave_plane', amplitude=0.3, frequency=3.0)
Tip

MFnMesh.setPoints() sets all vertex positions in a single call - this is orders of magnitude faster than calling cmds.xform(vtx, t=pos) for each vertex individually. For a 100k-vertex mesh, the API version runs in milliseconds while cmds can take minutes.

Creating Custom Commands

Custom commands integrate with Maya's undo system and can be invoked from MEL or Python just like built-in commands. They're ideal for operations that should be undoable and reusable.

Python
import maya.api.OpenMaya as om2
import maya.cmds as cmds

# Required: tells Maya this plugin uses API 2.0
maya_useNewAPI = True

class CenterPivotsCmd(om2.MPxCommand):
    """Custom command that centers pivots on all selected meshes."""

    COMMAND_NAME = 'centerSelectedPivots'

    def __init__(self):
        super().__init__()
        self._original_pivots = []

    @staticmethod
    def creator():
        return CenterPivotsCmd()

    @staticmethod
    def syntax():
        syntax = om2.MSyntax()
        return syntax

    def doIt(self, arg_list):
        """Called when the command is first executed."""
        sel = om2.MGlobal.getActiveSelectionList()

        self._original_pivots = []

        for i in range(sel.length()):
            dag_path = sel.getDagPath(i)
            fn_transform = om2.MFnTransform(dag_path)

            # Store original pivot for undo
            rp = fn_transform.rotatePivot(om2.MSpace.kTransform)
            sp = fn_transform.scalePivot(om2.MSpace.kTransform)
            self._original_pivots.append((dag_path, rp, sp))

        self.redoIt()

    def redoIt(self):
        """Called for redo - performs the actual operation."""
        for dag_path, _, _ in self._original_pivots:
            fn_transform = om2.MFnTransform(dag_path)
            origin = om2.MPoint(0, 0, 0)
            fn_transform.setRotatePivot(origin, om2.MSpace.kTransform, True)
            fn_transform.setScalePivot(origin, om2.MSpace.kTransform, True)
            # Use cmds for centerPivots since API lacks a direct equivalent
            cmds.xform(dag_path.fullPathName(), centerPivots=True)

    def undoIt(self):
        """Called for undo - restores original pivots."""
        for dag_path, rp, sp in self._original_pivots:
            fn_transform = om2.MFnTransform(dag_path)
            fn_transform.setRotatePivot(rp, om2.MSpace.kTransform, True)
            fn_transform.setScalePivot(sp, om2.MSpace.kTransform, True)

    def isUndoable(self):
        return True

def initializePlugin(plugin):
    fn_plugin = om2.MFnPlugin(plugin, 'TheTechnicalArtist', '1.0')
    try:
        fn_plugin.registerCommand(
            CenterPivotsCmd.COMMAND_NAME,
            CenterPivotsCmd.creator,
            CenterPivotsCmd.syntax
        )
    except Exception:
        om2.MGlobal.displayError(f'Failed to register command: {CenterPivotsCmd.COMMAND_NAME}')

def uninitializePlugin(plugin):
    fn_plugin = om2.MFnPlugin(plugin)
    try:
        fn_plugin.deregisterCommand(CenterPivotsCmd.COMMAND_NAME)
    except Exception:
        om2.MGlobal.displayError(f'Failed to deregister command: {CenterPivotsCmd.COMMAND_NAME}')

# Load with: cmds.loadPlugin('/path/to/center_pivots_cmd.py')
# Run with:  cmds.centerSelectedPivots()
Note

The maya_useNewAPI = True module-level variable is critical. Without it, Maya will attempt to load your plugin using API 1.0, and API 2.0 class constructors will fail. Always include this line at the top of your plugin files.

Creating Custom Nodes

Custom dependency graph nodes let you add new computation to Maya's node graph. This is how you'd build a custom deformer, a procedural generator, or a utility node that doesn't exist in Maya.

Python
import maya.api.OpenMaya as om2

maya_useNewAPI = True

class RemapRangeNode(om2.MPxNode):
    """
    Custom node that remaps a value from one range to another.
    Input range [inMin, inMax] maps to output range [outMin, outMax].
    """

    TYPE_NAME = 'remapRange'
    TYPE_ID = om2.MTypeId(0x00137800)  # Use a unique ID from your registered block

    # Attribute handles (class-level)
    input_value_attr = None
    in_min_attr = None
    in_max_attr = None
    out_min_attr = None
    out_max_attr = None
    clamp_attr = None
    output_attr = None

    def __init__(self):
        super().__init__()

    @staticmethod
    def creator():
        return RemapRangeNode()

    @staticmethod
    def initialize():
        fn_numeric = om2.MFnNumericAttribute()

        # Input attributes
        RemapRangeNode.input_value_attr = fn_numeric.create(
            'inputValue', 'iv', om2.MFnNumericData.kFloat, 0.0)
        fn_numeric.keyable = True
        fn_numeric.readable = False

        RemapRangeNode.in_min_attr = fn_numeric.create(
            'inputMin', 'imn', om2.MFnNumericData.kFloat, 0.0)
        fn_numeric.keyable = True
        fn_numeric.readable = False

        RemapRangeNode.in_max_attr = fn_numeric.create(
            'inputMax', 'imx', om2.MFnNumericData.kFloat, 1.0)
        fn_numeric.keyable = True
        fn_numeric.readable = False

        RemapRangeNode.out_min_attr = fn_numeric.create(
            'outputMin', 'omn', om2.MFnNumericData.kFloat, 0.0)
        fn_numeric.keyable = True
        fn_numeric.readable = False

        RemapRangeNode.out_max_attr = fn_numeric.create(
            'outputMax', 'omx', om2.MFnNumericData.kFloat, 10.0)
        fn_numeric.keyable = True
        fn_numeric.readable = False

        RemapRangeNode.clamp_attr = fn_numeric.create(
            'clamp', 'cl', om2.MFnNumericData.kBoolean, True)
        fn_numeric.keyable = True
        fn_numeric.readable = False

        # Output attribute
        RemapRangeNode.output_attr = fn_numeric.create(
            'output', 'out', om2.MFnNumericData.kFloat, 0.0)
        fn_numeric.writable = False
        fn_numeric.storable = False

        # Add all attributes
        RemapRangeNode.addAttribute(RemapRangeNode.input_value_attr)
        RemapRangeNode.addAttribute(RemapRangeNode.in_min_attr)
        RemapRangeNode.addAttribute(RemapRangeNode.in_max_attr)
        RemapRangeNode.addAttribute(RemapRangeNode.out_min_attr)
        RemapRangeNode.addAttribute(RemapRangeNode.out_max_attr)
        RemapRangeNode.addAttribute(RemapRangeNode.clamp_attr)
        RemapRangeNode.addAttribute(RemapRangeNode.output_attr)

        # Set attribute dependencies (inputs affect output)
        for attr in [RemapRangeNode.input_value_attr, RemapRangeNode.in_min_attr,
                     RemapRangeNode.in_max_attr, RemapRangeNode.out_min_attr,
                     RemapRangeNode.out_max_attr, RemapRangeNode.clamp_attr]:
            RemapRangeNode.attributeAffects(attr, RemapRangeNode.output_attr)

    def compute(self, plug, data):
        if plug != RemapRangeNode.output_attr:
            return

        # Read input values from the data block
        value = data.inputValue(RemapRangeNode.input_value_attr).asFloat()
        in_min = data.inputValue(RemapRangeNode.in_min_attr).asFloat()
        in_max = data.inputValue(RemapRangeNode.in_max_attr).asFloat()
        out_min = data.inputValue(RemapRangeNode.out_min_attr).asFloat()
        out_max = data.inputValue(RemapRangeNode.out_max_attr).asFloat()
        do_clamp = data.inputValue(RemapRangeNode.clamp_attr).asBool()

        # Remap calculation
        in_range = in_max - in_min
        if abs(in_range) < 1e-6:
            result = out_min
        else:
            normalized = (value - in_min) / in_range
            if do_clamp:
                normalized = max(0.0, min(1.0, normalized))
            result = out_min + normalized * (out_max - out_min)

        # Write output
        output_handle = data.outputValue(RemapRangeNode.output_attr)
        output_handle.setFloat(result)
        data.setClean(plug)

def initializePlugin(plugin):
    fn_plugin = om2.MFnPlugin(plugin, 'TheTechnicalArtist', '1.0')
    try:
        fn_plugin.registerNode(
            RemapRangeNode.TYPE_NAME,
            RemapRangeNode.TYPE_ID,
            RemapRangeNode.creator,
            RemapRangeNode.initialize
        )
    except Exception:
        om2.MGlobal.displayError(f'Failed to register node: {RemapRangeNode.TYPE_NAME}')

def uninitializePlugin(plugin):
    fn_plugin = om2.MFnPlugin(plugin)
    try:
        fn_plugin.deregisterNode(RemapRangeNode.TYPE_ID)
    except Exception:
        om2.MGlobal.displayError(f'Failed to deregister node: {RemapRangeNode.TYPE_NAME}')
Warning

The MTypeId must be globally unique. Autodesk allocates ID blocks to studios for production plugins. For personal or learning projects, use IDs in the 0x00000-0x7FFFF range, but be aware these could collide with other plugins. Never ship a plugin with an unregistered ID.

Performance Comparison

To illustrate why the API matters, here's a comparison of reading all vertex positions from a 100,000-vertex mesh using three different approaches:

Python
import maya.cmds as cmds
import pymel.core as pm
import maya.api.OpenMaya as om2
import time

MESH = 'highResMesh'

# --- Method 1: maya.cmds (slowest) ---
start = time.time()
vtx_count = cmds.polyEvaluate(MESH, vertex=True)
positions_cmds = []
for i in range(vtx_count):
    pos = cmds.xform(f'{MESH}.vtx[{i}]', query=True, translation=True, worldSpace=True)
    positions_cmds.append(pos)
elapsed_cmds = time.time() - start
print(f'cmds:    {elapsed_cmds:.3f}s for {vtx_count} vertices')

# --- Method 2: PyMEL (faster, but still wrapping) ---
start = time.time()
mesh_node = pm.PyNode(MESH)
shape = mesh_node.getShape()
positions_pm = [vtx.getPosition(space='world') for vtx in shape.vtx]
elapsed_pm = time.time() - start
print(f'PyMEL:   {elapsed_pm:.3f}s for {vtx_count} vertices')

# --- Method 3: OpenMaya 2.0 (fastest) ---
start = time.time()
sel = om2.MSelectionList()
sel.add(MESH)
dag = sel.getDagPath(0)
dag.extendToShape()
fn_mesh = om2.MFnMesh(dag)
positions_api = fn_mesh.getPoints(om2.MSpace.kWorld)
elapsed_api = time.time() - start
print(f'API 2.0: {elapsed_api:.3f}s for {len(positions_api)} vertices')

# Typical results on a 100k-vert mesh:
# cmds:    45.2s
# PyMEL:   12.8s
# API 2.0:  0.02s
Tip

The API isn't just a little faster - it's often 1000x faster for mesh data access. This is because MFnMesh.getPoints() copies the entire array in a single C++ call, while cmds makes one Python-to-C++ round trip per vertex. Any time you're processing geometry at scale, the API is the right tool.

Next Steps

You now have the foundations to build high-performance Maya tools. Apply these skills to Rigging with Python for practical character setup automation, or revisit PyMEL to see how it complements the API for different parts of your toolset.