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__.py as the entry point, and a blender_manifest.toml (Blender 4.2+ extensions) or bl_info dict (legacy add-ons) describing name, version, and supported Blender version.
- Define data — Create
PropertyGroup classes 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.Operator for each user action; implement poll() for availability, invoke() for interactive setup, and execute() for the actual work.
- Build UI panels — Subclass
bpy.types.Panel to expose operators and properties in the appropriate editor (3D viewport sidebar, properties editor, etc.).
- Register everything — List all classes in a
classes tuple and register/unregister them in matching register()/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
.zip extension 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__.py with clear register() and unregister() 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) or blender_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.translations or the "Category"/label conventions so user-facing text can be localized where the project supports it.
API Usage
- Use
bpy.types.Operator for actions, bpy.types.Panel for UI layout, and bpy.types.PropertyGroup for grouped, related settings.
- Define
bl_idname (lowercase, category.action format, e.g. object.apply_custom_modifier), bl_label, and bl_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) and execute() for the actual operation; invoke() should call self.execute(context) when it doesn't need extra interaction.
- Return
{'FINISHED'} on success or {'CANCELLED'} on failure/user-abort consistently — never return a bare None or an unrecognized string.
- Use
bpy.context.evaluated_depsgraph_get() and object.evaluated_get(depsgraph) when reading final scene state that includes modifiers, shape keys, or other dependency-graph-driven results.
Example: Operator, Panel, and Registration
bl_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
PropertyGroup classes 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
AddonPreferences subclass registered with bl_idname matching the add-on's module name.
- Use
PointerProperty, CollectionProperty, and fully-typed properties (StringProperty, FloatProperty, BoolProperty, etc.) with explicit name= and description= so tooltips and the Python API are self-documenting.
- Clean up custom properties,
bpy.app.handlers entries, timers, and keymaps during unregister() — anything added to a bpy.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
bmesh module (bmesh.from_edit_mesh, bmesh.new()) when programmatically editing mesh data instead of looping over mesh.vertices one 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 via bpy.ops, then call unregister() 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
context first.
- Hardcoding absolute asset paths that only exist on the developer's machine instead of using relative paths or
bpy.utils.resource_path.
1---2name: blender-python-addon3description: Best practices for writing Blender Python add-ons using the bpy API, covering operators, panels, properties, registration, and API-safe scripting. Use when creating a Blender add-on, defining bpy.types.Operator or Panel classes, registering PropertyGroup settings, writing register()/unregister() functions, working with bmesh, or debugging add-ons in Blender's Python console.4---5
6# Blender Python Add-on Development
7
8This 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.
9
10## Workflow for Building a Blender Add-on
11
121. **Scaffold the add-on** — Create a package with `__init__.py` as the entry point, and a `blender_manifest.toml` (Blender 4.2+ extensions) or `bl_info` dict (legacy add-ons) describing name, version, and supported Blender version.
132. **Define data** — Create `PropertyGroup` classes for grouped settings and register them on the appropriate ID type (e.g., `bpy.types.Scene.my_addon = PointerProperty(type=MyAddonSettings)`).
143. **Implement operators** — Subclass `bpy.types.Operator` for each user action; implement `poll()` for availability, `invoke()` for interactive setup, and `execute()` for the actual work.
154. **Build UI panels** — Subclass `bpy.types.Panel` to expose operators and properties in the appropriate editor (3D viewport sidebar, properties editor, etc.).
165. **Register everything** — List all classes in a `classes` tuple and register/unregister them in matching `register()`/`unregister()` functions; register property pointers alongside their classes.
176. **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.
187. **Package and ship** — Zip the add-on folder (or build a `.zip` extension package) and verify it installs cleanly via Blender's Preferences > Add-ons (or Extensions) panel.
19
20## Add-on Structure
21
22- Keep the add-on's entry point in `__init__.py` with clear `register()` and `unregister()` functions that mirror each other exactly (everything registered must be unregistered, in reverse order).
23- 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`.
24- Use `bl_info` (pre-4.2 legacy add-ons) or `blender_manifest.toml` (4.2+ extensions) matching the target Blender version and packaging model — don't mix conventions.
25- Keep UI labels concise and use `bpy.app.translations` or the `"Category"`/label conventions so user-facing text can be localized where the project supports it.
26
27## API Usage
28
29- Use `bpy.types.Operator` for actions, `bpy.types.Panel` for UI layout, and `bpy.types.PropertyGroup` for grouped, related settings.
30- Define `bl_idname` (lowercase, `category.action` format, e.g. `object.apply_custom_modifier`), `bl_label`, and `bl_options` (e.g., `{'REGISTER', 'UNDO'}`) explicitly on every operator.
31- 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.
32- Use `invoke()` for interactive setup (showing a dialog, reading mouse position) and `execute()` for the actual operation; `invoke()` should call `self.execute(context)` when it doesn't need extra interaction.
33- Return `{'FINISHED'}` on success or `{'CANCELLED'}` on failure/user-abort consistently — never return a bare `None` or an unrecognized string.
34- Use `bpy.context.evaluated_depsgraph_get()` and `object.evaluated_get(depsgraph)` when reading final scene state that includes modifiers, shape keys, or other dependency-graph-driven results.
35
36### Example: Operator, Panel, and Registration
37
38```python
39bl_info = {
40 "name": "Random Vertex Color",
41 "author": "Example",
42 "version": (1, 0, 0),
43 "blender": (4, 0, 0),
44 "category": "Mesh",
45}
46
47import bpy
48import random
49
50
51class MESH_OT_random_vertex_color(bpy.types.Operator):
52 """Assign a random color to the active vertex color layer"""
53
54 bl_idname = "mesh.random_vertex_color"
55 bl_label = "Randomize Vertex Colors"
56 bl_options = {"REGISTER", "UNDO"}
57
58 @classmethod
59 def poll(cls, context):
60 obj = context.active_object
61 return (
62 obj is not None
63 and obj.type == "MESH"
64 and obj.mode == "OBJECT"
65 and obj.data.color_attributes.active_color is not None
66 )
67
68 def execute(self, context):
69 obj = context.active_object
70 color_layer = obj.data.color_attributes.active_color
71 for data in color_layer.data:
72 data.color = (random.random(), random.random(), random.random(), 1.0)
73 obj.data.update()
74 self.report({"INFO"}, f"Randomized colors on '{obj.name}'")
75 return {"FINISHED"}
76
77
78class VIEW3D_PT_random_vertex_color(bpy.types.Panel):
79 bl_label = "Vertex Color Tools"
80 bl_idname = "VIEW3D_PT_random_vertex_color"
81 bl_space_type = "VIEW_3D"
82 bl_region_type = "UI"
83 bl_category = "Tool"
84
85 def draw(self, context):
86 layout = self.layout
87 layout.operator(MESH_OT_random_vertex_color.bl_idname, icon="COLOR")
88
89
90classes = (
91 MESH_OT_random_vertex_color,
92 VIEW3D_PT_random_vertex_color,
93)
94
95
96def register():
97 for cls in classes:
98 bpy.utils.register_class(cls)
99
100
101def unregister():
102 for cls in reversed(classes):
103 bpy.utils.unregister_class(cls)
104
105
106if __name__ == "__main__":
107 register()
108```
109
110## Data and Properties
111
112- Register custom properties through `PropertyGroup` classes instead of stuffing loose global state into module-level variables, which don't survive file reload and aren't undo-safe.
113- Store add-on preferences (API keys, default paths, UI toggles that persist across files) in an `AddonPreferences` subclass registered with `bl_idname` matching the add-on's module name.
114- Use `PointerProperty`, `CollectionProperty`, and fully-typed properties (`StringProperty`, `FloatProperty`, `BoolProperty`, etc.) with explicit `name=` and `description=` so tooltips and the Python API are self-documenting.
115- Clean up custom properties, `bpy.app.handlers` entries, timers, and keymaps during `unregister()` — anything added to a `bpy.types.*` class or a handler list must be explicitly removed.
116
117## Safety and Performance
118
119- Never run destructive scene operations (deleting objects, overwriting files) without explicit user action — no silent auto-execution on load for anything destructive.
120- 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.
121- Batch mesh edits and use the `bmesh` module (`bmesh.from_edit_mesh`, `bmesh.new()`) when programmatically editing mesh data instead of looping over `mesh.vertices` one at a time for structural changes.
122- 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.
123- Keep file paths configurable and resolve them with Blender path utilities (`bpy.path.abspath`, `bpy.utils.resource_path`) instead of hardcoding OS-specific paths.
124
125## Testing and Debugging
126
127- 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.
128- Add smoke tests that import the add-on module, call `register()`, run each core operator via `bpy.ops`, then call `unregister()` cleanly with no errors or leftover state.
129- Use `self.report({'ERROR'}, "message")` or `{'WARNING'}`/`{'INFO'}` for user-facing operator feedback instead of printing to the console, which most users never see.
130- 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.
131
132## Common Mistakes
133
134- Forgetting to unregister classes, handlers, timers, and keymaps in `unregister()`, leaving Blender in a broken state after disabling the add-on.
135- Mutating Blender data (adding objects, changing mesh data) from inside a `Panel.draw()` method — `draw()` must only read data and lay out UI.
136- Assuming an active object, a non-empty selection, or a specific mode (Object/Edit/Sculpt) exists without checking `context` first.
137- Hardcoding absolute asset paths that only exist on the developer's machine instead of using relative paths or `bpy.utils.resource_path`.