Improve your Blender python coding by translating blender operator calls into native API calls. When coding in blender, there are many ways to accomplish the same task. However, some methods are more efficient and faster than others. This is a reference page for “de-op’ing” your code to make it faster and more efficient
Want to learn how to use Python in Blender?
Blender Operators
In general, when a user presses a button in blender, it invokes an operator. This is a special blender python class which runs code from the interface. From the blender info window you can see the order of operators used, allowing you to make macro-like scripts as you could in microsoft office or other programs. While this can be used to quickly write scripts, it can leads to troubles.
The problem with operators
Blender operators are meant to be used by someone pressing a button, thus running some code and finishing with an execution status. Ideally, they are not used in the middle of other code for a variety of reasons. A few of these concerns include:
- They cannot return values that are usable beyond how they completed (e.g. ‘FINISHED’ or “CANCELLED’)
- For example, bpy.ops.duplicate() will not return the newly created object which you might want. Instead, you have to obtain it by bpy.context.scene.active_object
- Running an operator can modify current context in a non-desired way
- If you wanted to duplicate an object in the middle of a script, using the operator will clear the user’s current selection and set the new object as selected and active. If you did not want your code to do this, you would then have to start by getting the initial selection and then re-applying this selection at the end of the script.
- They force you to change state
- Inherently, operators are context-aware and you cannot run EDIT_MESH operators in OBJECT mode by design. If you want your own code to edit mesh values from object mode, you would then have to sprinkle
bpy.ops.object.mode_set()
operator calls into your scripts to change between mode=’OBJECT’ and mode=’EDIT’ based on which operators you are trying to run. You even end up “sandwiching” mode_set() to enter a specific mode for one operator call and then immediately switch back. In essence, you then run three operator calls when you wanted to only run one.
- Inherently, operators are context-aware and you cannot run EDIT_MESH operators in OBJECT mode by design. If you want your own code to edit mesh values from object mode, you would then have to sprinkle
- They run slower than native blender python API calls*
- Directly true due to the extra interface related code, such as changing selections in some cases and other side operations that you as a developer aren’t necessarily looking to do. This is unnecessary processing that slows down your code.
- Indirectly true for even more reasons, including but not limited to the bullet points above. Having to counteract unwanted selection changes and mode sets all add processing time.
- * See the notes at the bottom of the page, as sometimes operator code can run faster than low level API calls.
Instead, you could use direct python API calls to do what you want – and only what you want. Use this page as a reference for translating operator code into lower-level python API calls. The guide will expand over time, and is sorted roughly by most common use.
Operator translations
Select All/None - bpy.ops.object.select_all
While you can use bpy.ops.object.select_all(action) with action = “TOGGLE”, “SELECT”, or “DESELECT”, to perform the same action and ensure even hidden objects and objects on other layers have their selection state updated, you could instead use
# select none for ob in context.scene.objects: ob.select=False # select all if visible for ob in context.scene.objects: if ob.hide == False: ob.select=False
Speed note: The API calls appear to run barely faster, but overall execution time is fast and not different enough between operator and python calls.
Move or translate - bpy.ops.transform.translate
To move an object, definitely make use you use object.location instead of the operator.
ob.location = (1,2,3) # assign new location ob.location[2] += 1 # move the object up 1 unit in the z-direction reference_location = ob.location.copy() # Be sure to copy location if you want to save the values
Delete object - bpy.ops.object.delete
The below will let you delete an object without knowing or modify its selection and layer, and will not affect any other objects in the scene.
ob = bpy.data.objects["some-object"] # object to delete bpy.context.scene.objects.unlink(ob) # actively remove it from the current scene, careful if in other scenes ob.user_clear() # clears all users, e.g. from groups scenes bpy.data.objects.remove(ob) # alternatively, if you just want to remove it from the active scene context.scene.objects.unlink(ob)
If you used the operator instead, you would need to first deselect all objects and make the one object desired for deleting active, then most likely go back and re-apply the initial selection for consistency.
Speed note: See the notes below, in short the operators calls can be significantly slower than the equivalent API call.
Using simple cubes, operator versus API call (time is in seconds)
- Deleting 3 cubes: Operator (0.00036) is slower than API call (0.00008) by a factor of 4.40
- Deleting 51 cubes: Operator (0.00538) is slower than API call (0.00144) by a factor of 3.71
- Deleting 343 cubes: Operator (0.0920) is slower than API call (0.0207) by a factor of 4.45
- Deleting 2402 cubes: Operator (14.519) is slower than API call (2.687) by a factor of 5.40
Running the same tests against a more data-heavy Suzanne model (subsurf level 1 applied, UV map and vertex group, 2,012 verts/1,968 poly, no linked data),
- Deleting 3 objects: Operator (0.00151) is slower than API call (0.00129) by a factor of 1.17
- Deleting 51 objects: Operator (0.1434) is significantly slower than API call (0.0016) by a factor of 91.4
- Deleting 343 objects: Operator (7.014) is significantly slower than API call (0.035) by a factor of 199
- Deleting 2402 objects: Operator (332.28) is significantly slower than API call (3.03) by a factor of 109
Duplicating objects - bpy.ops.object.duplicate_move & bpy.ops.object.duplicate
Duplicating using the above operator changes the active selection, active object, and does not conveniently return to you the newly created object (though you could grab it via context.object immediately after the operator runs). It can also be tricky as this operator will also duplicate even hidden objects, so running bpy.ops.object.select_all(action=’DESELECT’) prior to duplication may not accomplish what you are hoping. Instead, use the following:
ob = bpy.data.objects["some-object"] # object to duplicate # first, create the datablock new_ob = bpy.data.objects.new( ob.name, ob.data.copy()) context.scene.objects.link(new_ob) # adds the object to the active scene new_ob.location = (0,0,0) # set new location, or leave it as-is to keep the same location as the original object. Note, the original selection is unchanged even if the original object wasn't selected or active.
If you wanted to make a linked duplicate, note the minor change of the second line
ob = bpy.data.objects["some-object"] # object to duplicate new_ob = bpy.data.objects.new( ob.name, ob.data) # this will make a linked copy of ob.data context.scene.objects.link(new_ob) # adds the object to the active scene new_ob.location = (0,0,0) # relocate
Speed note: It is my observation that the operator can run faster when duplicating a larger quantity of objects, while duplicating a small number of objects at once is quickest via python API calls.
Simple cube duplication test (times are in seconds):
- Duplicating 3 cubes: Operator (0.00059) is slower than API call (0.00026) by a factor of 2.3
- Duplicating 51 cubes: Operator (0.00484) is about equal but slower than API call (0.00462) by a factor of 1.05
- Duplicating 343 cubes: Operator (0.0732) is faster than API call (0.0913) by a factor of 1.24
- Duplicating 2402 cubes: Operator (5.092) is faster than API call (8.307) by a factor of 1.63
Results when the same test is run on a more complex object (Suzanne model subsurf level 1 applied, with UV map and vertex group, 2,012 verts/1,968 poly):
- Duplicating 3 objects: Operator (0.00182) is slower than API call (0.00129) by a factor of 1.41
- Duplicating 51 objects: Operator (0.0181) is slower than API call (0.0158) by a factor of 1.15
- Duplicating 343 objects: Operator (0.198) is about equal but slower than API call (0.186) by a factor of 1.07
- Duplicating 2402 objects: Operator (7.08) is faster than API call (10.98) by a factor of 1.55
Additional notes
The copying method shown above does not copy everything about the source object. For examples, modifiers and vertex groups (and some other attributes) will not be carried over. You can, however, add them back selectively this way if you choose:
# ob is the original object, new_ob is the new using the above code for mod_src in ob.modifiers: # create one modifier of the same type on the new object dest = new_ob.modifiers.new(mod_src.name, mod_src.type) # Copy all attributes (settings, drop downs, etc) from the source modifier # skipping anything that is read only since those cannot be changed anyways. properties = [p.identifier for p in mod_src.bl_rna.properties if not p.is_readonly] for prop in properties: setattr(dest, prop, getattr(mod_src, prop))
Group objects - bpy.ops.group.create
Replace bpy.ops.group.create(name="GroupName")
with the code below to more directly create or modify groups with selected (or any) objects.
group = bpy.data.groups.new("GroupName") # note we get a direct ref to new group for ob in context.selected_objects: # or whichever list of objects desired group.objects.link(ob) # now linked to the group, green outline in 3d view
One notable benefit of the above is we get the direct group reference. Using the operator, you would instead need to compare the list of groups before and after creating the new one to be sure you have the right reference. While you could supply name=”GroupName” into the operator call, blender could end up creating a group named “GroupName.001” instead if the first group already existed. Thus, it would be a mistake to try and directly reference the newly made group by name unless you checked for the name explicitly ahead of time that it was safe and unused.
Remove All Group Objects - bpy.ops.group.objects_remove_all
This can be replaced by a simple for-loop as below. One clear benefit of using python API calls in this case is that it is not always clear what the active group may be in the same way the active object may be, so you can be more explicit on which group it operates.
group = bpy.data.groups["group-name"] # for loop is equivalent to bpy.ops.group.objects_remove_all() for ob in group.objects: group.objects.unlink(ob) # remove from group
If you’re looking to completely remove a group, blender does not provide a single operator to completely remove a group from a blender file – so the best way is via python.
group = bpy.data.groups["group-name"] # We still want to remove all objects first for ob in group.objects: group.objects.unlink(ob) # remove from group # then, remove the group itself - no equivalent operator # group.user_clear() # in some cases, this may be needed bpy.data.groups.remove(group)
Add Group Instance - bpy.ops.object.group_instance_add
Adding a group instance from Shift-A > Group is equivalent to the steps below. It is more stable and can directly give you the name of the resulting added object.
def addGroupInstance(group, location): scene = bpy.context.scene ob = bpy.data.objects.new(group.name, None) ob.dupli_type = 'GROUP' ob.dupli_group = group # if only group name known and not direct ID: # ob.dupli_group = bpy.data.groups.get(group.name) ob.location = location # or for example, ob.location = scene.cursor_location scene.objects.link(ob) # this actually adds to the active scene # ob.select = True # if desirable, see above if you want to deselect other items or not return ob # the reference empty created # use the function group = bpy.data.groups["group-name"] groupInstance = addGroupInstance(group.name,(0,0,0))
New image - bpy.ops.image.new
When creating a new image internally in blender using the operator, you don’t immediately get the reference to the image just created. To find it, you would have to compare the list of images before and after the operator. Rather than that, you can use the constructor that comes with data.images:
# Operator method to create a gray, half-transparent 512x512 image images_before = list(bpy.data.images) # copy the list ahead to get bpy.ops.image.new( name="newImage", height=512, width=512, color=(0.5,0.5,0.5,0.5), alpha=True, generated_type="BLANK", float=False ) images_after = list(bpy.data.images) differences = set(images_after).difference(images_before) # do difference of image list before/after new_image = list(differences)[0] # extract the one new image created # Low level api method to create a gray, half-transparent 512x512 image new_img = bpy.data.images.new( name="newImage", height=512, width=512, alpha=True ) # we already have the reference, but need to set initial color. # For that, the following will work. # Note that new_img.pixels is a long, 1D list in the format of [# channels e.g. rgba]*[# total pixel count e.g. 512*512] new_img.pixels = [0.5,0.5,0.5,0.5]*(len(new_img.pixels)/4)
Now for the timing comparison. These tests were each run 100x with each set done in a refreshed blender instance, variances within sets were all statistically non-significant. Timings are in seconds
NxN image | Operator Black | api Black | Operator Color | api Color |
---|---|---|---|---|
256px | 0.000390 | 0.000243 | 0.000367 | 0.006842 |
2048px | 0.007365 | 0.007104 | 0.007478 | 0.557998 |
8192px | 0.171569 | 0.177132 | 0.174332 | 10.258755 |
Generally speaking, creating a single image is quite quick and likely isn’t the critical path for any operator unless it’s trying to generate a large number of images. Regardless, it appears the low level api and operator both have very similar timings for small and large images alike. The operator has a leg up when it comes to setting initial colors; the lower level api takes quite a hit especially at higher resolutions for that second line operation generating all those color values.
New VSE image sequence - bpy.ops.sequencer.image_strip_add
It is quite fast and easy to create a new image sequence in the VSE, which contains a series of sequential image files (frame_001.png, frame_002.png, etc). Use of this operator will fail if you don’t have the right context available, such as you might in a command line environment. Instead, use lower-level API calls to add the sequence – and the tricky part, you need to individually add each element of the image sequence after the fact.
# Get your sequence of image files, note here the list of files here is of a dictionary files = [ {"name":f} for f in os.path.listdir(FILEPATH) if f.endswith(".png") ] # This won't work from a different context e.g. on command line, but would work from within a script or other operator with valid context # This is the operator to remove the use of bpy.ops.sequencer.image_strip_add( directory=img_folder, files=files, # this requires each element to have the form of {"name":filepath} show_multiview=False, frame_start=1, frame_end=len(files), channel=1)
Now take a look at the lower-level implementation without operators:
# Get your sequence of image files files = [ f for f in os.path.listdir(FILEPATH) if f.endswith(".png") ] # This works in all cases, and is in the spirit of lower level, non operator code. # Create the sequence object, which will be just 1 frame in length seq = bpy.context.scene.sequence_editor.sequences.new_image( name="Frame Sequence", filepath=os.path.join(FILEPATH,files[0]), # a full filepath for the first image channel=1, frame_start=1) # now add each additional frame for f in files[1:]: seq.elements.append(f)
I like this translation as it helps showcase the underlining data structure of a sequence object: a primary filepath, plus a list of elements referring to other files in the same folder. Timing comparisons to come.
More examples of operator translations are coming soon. Give a shout if there are any other operators you want to include or have recommended translations to include!
Want to learn more?
Some notes
Please note that from a technical standpoint, there is nothing wrong with using operator calls in your scripts or even as lines of code within your own operators. However, in many cases – particularly when there are few objects involved – using lower-level python API calls instead of the equivalent operators can reduce processing time. But, in some cases operators can execute faster than the API calls. One noteworthy example of when this can be true is duplicating a lot of objects at once. For instance, note the following trend on a simple cube-duplication test (times are in seconds):
- Duplicating 3 objects: Operator (0.00182) is slower than API call (0.00129) by a factor of 1.41
- Duplicating 51 objects: Operator (0.0181) is slower than API call (0.0158) by a factor of 1.15
- Duplicating 343 objects: Operator (0.198) is about equal but slower than API call (0.186) by a factor of 1.07
- Duplicating 2402 objects: Operator (7.08) is faster than API call (10.98) by a factor of 1.55
So a conclusion could be that API calls are more efficient for duplicating small numbers of objects and less so for larger numbers of objects. This is not always true, as for example deleting objects can be significantly faster by using API calls:
- Deleting 3 objects: Operator (0.00151) is slower than API call (0.00129) by a factor of 1.17
- Deleting 51 objects: Operator (0.1434) is significantly slower than API call (0.0016) by a factor of 91.4
- Deleting 343 objects: Operator (7.014) is significantly slower than API call (0.035) by a factor of 199
- Deleting 2402 objects: Operator (332.28) is significantly slower than API call (3.03) by a factor of 109
In that last extreme example, the python API calls took 3 seconds while the operator call took over 5 minutes. For deleting objects, the API call seems to always be faster and can save a lot of time when deleting a large number of objects. The end point is: Be sure to use your judgement and run test timings. If speed is a primary concern, test scenarios out using the time library:
import time t0 = time.time() bpy.ops.object.select_all(action='SELECT') # run some code t1 = time.time() print("The time to run that code was: ",t1-t0)