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.
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.
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.
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()}')
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()}')
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.
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
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)
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.
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()
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.
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}')
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:
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
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.
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.