Blender Python Add-on Development
This skill covers building Blender add-ons with the bpy API, including add-on structure, operators, panels, properties, safe scene manipulation, and testing across Blender versions.
Workflow for Building a Blender Add-on
- Scaffold the add-on — Create a package with
__init__.pyas the entry point, and ablender_manifest.toml(Blender 4.2+ extensions) orbl_infodict (legacy add-ons) describing name, version, and supported Blender version. - Define data — Create
PropertyGroupclasses for grouped settings and register them on the appropriate ID type (e.g.,bpy.types.Scene.my_addon = PointerProperty(type=MyAddonSettings)). - Implement operators — Subclass
bpy.types.Operatorfor each user action; implementpoll()for availability,invoke()for interactive setup, andexecute()for the actual work. - Build UI panels — Subclass
bpy.types.Panelto expose operators and properties in the appropriate editor (3D viewport sidebar, properties editor, etc.). - Register everything — List all classes in a
classestuple and register/unregister them in matchingregister()/unregister()functions; register property pointers alongside their classes. - Test in a clean profile — Launch Blender with
--factory-startup, install the add-on, enable it, exercise each operator, then disable it and confirm nothing is left behind. - Package and ship — Zip the add-on folder (or build a
.zipextension package) and verify it installs cleanly via Blender's Preferences > Add-ons (or Extensions) panel.
Add-on Structure
- Keep the add-on's entry point in
__init__.pywith clearregister()andunregister()functions that mirror each other exactly (everything registered must be unregistered, in reverse order). - Group operators, panels, properties, preferences, and utility code into separate modules once the add-on grows past a trivial size; import and register them from
__init__.py. - Use
bl_info(pre-4.2 legacy add-ons) orblender_manifest.toml(4.2+ extensions) matching the target Blender version and packaging model — don't mix conventions. - Keep UI labels concise and use
bpy.app.translationsor the"Category"/label conventions so user-facing text can be localized where the project supports it.
API Usage
- Use
bpy.types.Operatorfor actions,bpy.types.Panelfor UI layout, andbpy.types.PropertyGroupfor grouped, related settings. - Define
bl_idname(lowercase,category.actionformat, e.g.object.apply_custom_modifier),bl_label, andbl_options(e.g.,{'REGISTER', 'UNDO'}) explicitly on every operator. - Validate context in
poll()before allowing an operator to run — check for an active object, correct mode, or valid selection so the operator button greys out instead of erroring. - Use
invoke()for interactive setup (showing a dialog, reading mouse position) andexecute()for the actual operation;invoke()should callself.execute(context)when it doesn't need extra interaction. - Return
{'FINISHED'}on success or{'CANCELLED'}on failure/user-abort consistently — never return a bareNoneor an unrecognized string. - Use
bpy.context.evaluated_depsgraph_get()andobject.evaluated_get(depsgraph)when reading final scene state that includes modifiers, shape keys, or other dependency-graph-driven results.
Example: Operator, Panel, and Registration
pythonbl_info = { "name": "Random Vertex Color", "author": "Example", "version": (1, 0, 0), "blender": (4, 0, 0), "category": "Mesh", } import bpy import random class MESH_OT_random_vertex_color(bpy.types.Operator): """Assign a random color to the active vertex color layer""" bl_idname = "mesh.random_vertex_color" bl_label = "Randomize Vertex Colors" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): obj = context.active_object return ( obj is not None and obj.type == "MESH" and obj.mode == "OBJECT" and obj.data.color_attributes.active_color is not None ) def execute(self, context): obj = context.active_object color_layer = obj.data.color_attributes.active_color for data in color_layer.data: data.color = (random.random(), random.random(), random.random(), 1.0) obj.data.update() self.report({"INFO"}, f"Randomized colors on '{obj.name}'") return {"FINISHED"} class VIEW3D_PT_random_vertex_color(bpy.types.Panel): bl_label = "Vertex Color Tools" bl_idname = "VIEW3D_PT_random_vertex_color" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Tool" def draw(self, context): layout = self.layout layout.operator(MESH_OT_random_vertex_color.bl_idname, icon="COLOR") classes = ( MESH_OT_random_vertex_color, VIEW3D_PT_random_vertex_color, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) if __name__ == "__main__": register()
Data and Properties
- Register custom properties through
PropertyGroupclasses instead of stuffing loose global state into module-level variables, which don't survive file reload and aren't undo-safe. - Store add-on preferences (API keys, default paths, UI toggles that persist across files) in an
AddonPreferencessubclass registered withbl_idnamematching the add-on's module name. - Use
PointerProperty,CollectionProperty, and fully-typed properties (StringProperty,FloatProperty,BoolProperty, etc.) with explicitname=anddescription=so tooltips and the Python API are self-documenting. - Clean up custom properties,
bpy.app.handlersentries, timers, and keymaps duringunregister()— anything added to abpy.types.*class or a handler list must be explicitly removed.
Safety and Performance
- Never run destructive scene operations (deleting objects, overwriting files) without explicit user action — no silent auto-execution on load for anything destructive.
- Avoid blocking the UI thread in modal operators; use
context.window_manager.event_timer_add()with a modal state machine for long-running operations instead of a tight loop. - Batch mesh edits and use the
bmeshmodule (bmesh.from_edit_mesh,bmesh.new()) when programmatically editing mesh data instead of looping overmesh.verticesone at a time for structural changes. - Avoid repeatedly scanning large scenes or recomputing expensive data inside
draw()methods —draw()runs on every UI redraw, so cache results and invalidate them only when the underlying data changes. - Keep file paths configurable and resolve them with Blender path utilities (
bpy.path.abspath,bpy.utils.resource_path) instead of hardcoding OS-specific paths.
Testing and Debugging
- Test in both a clean Blender profile (
blender --factory-startup) to catch hidden dependencies on other add-ons, and in a representative production scene to catch performance and data-shape issues. - Add smoke tests that import the add-on module, call
register(), run each core operator viabpy.ops, then callunregister()cleanly with no errors or leftover state. - Use
self.report({'ERROR'}, "message")or{'WARNING'}/{'INFO'}for user-facing operator feedback instead of printing to the console, which most users never see. - Keep version-specific API differences (e.g., API changes between Blender 3.x and 4.x) isolated behind small helper functions so the rest of the add-on doesn't need version checks scattered throughout.
Common Mistakes
- Forgetting to unregister classes, handlers, timers, and keymaps in
unregister(), leaving Blender in a broken state after disabling the add-on. - Mutating Blender data (adding objects, changing mesh data) from inside a
Panel.draw()method —draw()must only read data and lay out UI. - Assuming an active object, a non-empty selection, or a specific mode (Object/Edit/Sculpt) exists without checking
contextfirst. - Hardcoding absolute asset paths that only exist on the developer's machine instead of using relative paths or
bpy.utils.resource_path.

