
Blender Syntax Operators
- 3 installs
- 30 repo stars
- Updated July 8, 2026
- openaec-foundation/blender-bonsai-ifcopenshell-sverchok-claude-skill-package
Helps with ai & agent building tasks.
About
blender-syntax-operators is a Claude Code skill for ai & agent building. It helps you ship faster with AI-assisted development.
- blender-syntax-operators
- AI & Agent Building
- AI-coding skill
Blender Syntax Operators by the numbers
- 3 all-time installs (skills.sh)
- Ranked #13,677 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/blender-bonsai-ifcopenshell-sverchok-claude-skill-package --skill blender-syntax-operatorsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 30 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/blender-bonsai-ifcopenshell-sverchok-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
blender-syntax-operators
Quick Reference
Critical Warnings
ALWAYS return a set from execute(), invoke(), and modal() — e.g., {'FINISHED'}, {'CANCELLED'}, {'RUNNING_MODAL'}. Returning a plain string or None crashes Blender.
ALWAYS use context.temp_override() in Blender 4.0+. Dict-based context overrides (bpy.ops.foo(override_dict, ...)) were REMOVED in 4.0.
ALWAYS set bl_options = {'REGISTER', 'UNDO'} for operators that modify scene data. Omitting 'UNDO' means users cannot Ctrl+Z the operation.
ALWAYS implement poll() as a @classmethod. Forgetting @classmethod causes a TypeError at registration.
NEVER call bpy.ops.* inside Panel.draw() — draw callbacks are read-only. Use operator buttons via layout.operator() instead.
NEVER use uppercase letters in the category part of bl_idname. The format is "category.operator_name" — both parts MUST be lowercase with underscores.
NEVER store mutable state as class-level attributes on operators expecting per-instance behavior. Use self instance attributes set in invoke() or execute(), or use operator properties.
Operator Decision Tree
Need to create a Blender operation?
│
├─ Runs once, no user interaction needed?
│ └─ Implement execute() only
│ └─ Set invoke = execute (optional shorthand)
│
├─ Needs a dialog/popup before running?
│ └─ Implement invoke() → wm.invoke_props_dialog(self)
│ └─ Implement draw() for dialog layout
│ └─ Implement execute() for the actual work
│
├─ Needs continuous event handling (drag, timer, mouse)?
│ └─ Implement invoke() → wm.modal_handler_add(self) + return {'RUNNING_MODAL'}
│ └─ Implement modal() for event processing
│ └─ Implement cancel() for cleanup
│
└─ Needs confirmation popup?
└─ Implement invoke() → wm.invoke_confirm(self, event)
└─ Implement execute() for the confirmed actionVersion Compatibility Matrix
| Feature | Blender 3.x | Blender 4.0+ | Blender 4.2+ |
|---|---|---|---|
| Context override dict | bpy.ops.foo(override, ...) | REMOVED | REMOVED |
context.temp_override() | Available from 3.2 | REQUIRED | REQUIRED |
'MODAL_PRIORITY' bl_option | Not available | Not available | Available |
bl_idname format | "CAT.name" or "cat.name" | "cat.name" (lowercase enforced) | "cat.name" |
| Extension manifest | bl_info dict | bl_info or blender_manifest.toml | blender_manifest.toml preferred |
---
Essential Patterns
Pattern 1: Minimal Operator (execute only)
import bpy
class MYCAT_OT_simple_action(bpy.types.Operator):
"""Tooltip for the operator"""
bl_idname = "mycat.simple_action"
bl_label = "Simple Action"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None
def execute(self, context):
context.active_object.location.z += 1.0
self.report({'INFO'}, "Moved object up")
return {'FINISHED'}
def register():
bpy.utils.register_class(MYCAT_OT_simple_action)
def unregister():
bpy.utils.unregister_class(MYCAT_OT_simple_action)Pattern 2: Operator with Properties
class MYCAT_OT_scale_object(bpy.types.Operator):
"""Scale the active object by a specified factor"""
bl_idname = "mycat.scale_object"
bl_label = "Scale Object"
bl_options = {'REGISTER', 'UNDO'}
# Operator properties — shown in F9 redo panel and invoke dialogs
factor: bpy.props.FloatProperty(
name="Scale Factor",
default=2.0,
min=0.01,
max=100.0,
description="Factor to scale the object by",
)
uniform: bpy.props.BoolProperty(
name="Uniform",
default=True,
description="Scale uniformly on all axes",
)
axis: bpy.props.EnumProperty(
name="Axis",
items=[
('X', "X", "Scale on X axis only"),
('Y', "Y", "Scale on Y axis only"),
('Z', "Z", "Scale on Z axis only"),
],
default='X',
)
@classmethod
def poll(cls, context):
return context.active_object is not None
def execute(self, context):
obj = context.active_object
if self.uniform:
obj.scale *= self.factor
else:
setattr(obj.scale, self.axis.lower(),
getattr(obj.scale, self.axis.lower()) * self.factor)
return {'FINISHED'}Pattern 3: Invoke with Props Dialog
class MYCAT_OT_create_grid(bpy.types.Operator):
"""Create a grid of objects with user-specified parameters"""
bl_idname = "mycat.create_grid"
bl_label = "Create Grid"
bl_options = {'REGISTER', 'UNDO'}
count_x: bpy.props.IntProperty(name="Count X", default=3, min=1, max=50)
count_y: bpy.props.IntProperty(name="Count Y", default=3, min=1, max=50)
spacing: bpy.props.FloatProperty(name="Spacing", default=2.0, min=0.1)
def invoke(self, context, event):
# Shows a dialog with the properties; execute() runs on OK
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "count_x")
layout.prop(self, "count_y")
layout.prop(self, "spacing")
def execute(self, context):
for x in range(self.count_x):
for y in range(self.count_y):
bpy.ops.mesh.primitive_cube_add(
location=(x * self.spacing, y * self.spacing, 0)
)
self.report({'INFO'}, f"Created {self.count_x * self.count_y} objects")
return {'FINISHED'}Pattern 4: Modal Operator
class MYCAT_OT_modal_draw(bpy.types.Operator):
"""Interactive modal operator with timer"""
bl_idname = "mycat.modal_draw"
bl_label = "Modal Draw"
bl_options = {'REGISTER', 'UNDO'}
_timer = None
def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
# Periodic update logic here
pass
if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
# Finish on left click
self.cancel(context)
return {'FINISHED'}
return {'PASS_THROUGH'}
def invoke(self, context, event):
self._timer = context.window_manager.event_timer_add(
0.1, window=context.window
)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
if self._timer is not None:
context.window_manager.event_timer_remove(self._timer)
self._timer = NonePattern 5: Context Override (4.0+ required)
# Override active object for operator execution
with bpy.context.temp_override(active_object=obj, selected_objects=[obj]):
bpy.ops.object.shade_smooth()
# Override area type for viewport operators
def run_in_viewport(operator_call):
"""Execute an operator that requires a VIEW_3D area context."""
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
with bpy.context.temp_override(window=window, area=area):
return operator_call()
raise RuntimeError("No VIEW_3D area found")
# Usage:
run_in_viewport(lambda: bpy.ops.view3d.snap_cursor_to_center())Pattern 6: Confirmation Dialog
class MYCAT_OT_delete_all(bpy.types.Operator):
"""Delete all objects with confirmation"""
bl_idname = "mycat.delete_all"
bl_label = "Delete All Objects"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return len(bpy.data.objects) > 0
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
def execute(self, context):
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
self.report({'WARNING'}, "All objects deleted")
return {'FINISHED'}---
Common Operations
Calling Operators from Scripts
# Direct call: uses current context
bpy.ops.mesh.primitive_cube_add(size=2.0, location=(0, 0, 1))
# With context override (Blender 4.0+)
with bpy.context.temp_override(active_object=obj):
bpy.ops.object.modifier_apply(modifier="Boolean")
# Check if operator can run before calling
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='EDIT')Registration and Menus
# Register operator and add to menu
def menu_func(self, context):
self.layout.operator(MYCAT_OT_simple_action.bl_idname)
def register():
bpy.utils.register_class(MYCAT_OT_simple_action)
bpy.types.VIEW3D_MT_object.append(menu_func)
def unregister():
bpy.types.VIEW3D_MT_object.remove(menu_func)
bpy.utils.unregister_class(MYCAT_OT_simple_action)Operator Reporting
# Report types: 'DEBUG', 'INFO', 'OPERATOR', 'WARNING', 'ERROR', 'ERROR_INVALID_INPUT'
self.report({'INFO'}, "Operation completed")
self.report({'WARNING'}, "Check the result")
self.report({'ERROR'}, "Something went wrong") # Shows in status bar, does NOT raisebl_idname Naming Convention
CLASS_OT_operator_name
│ │ │
│ │ └─ snake_case descriptive name
│ └───── OT = Operator Type (ALWAYS "OT" for operators)
└──────────── UPPERCASE category prefix (matches addon/module)The bl_idname string format: "category.operator_name" — both parts lowercase. The class name format: CATEGORY_OT_operator_name — category uppercase, rest snake_case.
# CORRECT:
class MESH_OT_add_custom(bpy.types.Operator):
bl_idname = "mesh.add_custom" # lowercase.lowercase
# WRONG: bl_idname has uppercase:
class MESH_OT_add_custom(bpy.types.Operator):
bl_idname = "Mesh.AddCustom" # WILL FAIL at registrationbl_options Reference
| Flag | Use When |
|---|---|
'REGISTER' | ALWAYS — makes operator visible in info log and F3 search |
'UNDO' | Operator modifies scene data (objects, meshes, materials) |
'UNDO_GROUPED' | Multiple rapid calls should be one undo step (e.g., timer-based updates) |
'BLOCKING' | Modal operator should block ALL other event handlers |
'GRAB_CURSOR' | Modal with mouse movement should wrap cursor at screen edges |
'GRAB_CURSOR_X' | Wrap cursor on X axis only |
'GRAB_CURSOR_Y' | Wrap cursor on Y axis only |
'INTERNAL' | Operator should NOT appear in F3 search menu |
'PRESET' | Show preset selector in operator properties panel |
'MACRO' | Operator is a macro containing sub-operators |
'MODAL_PRIORITY' | (4.2+) Receive events before other modal operators |
Return Values
| Value | When to Use |
|---|---|
{'FINISHED'} | Operator completed successfully |
{'CANCELLED'} | Operator was cancelled, no changes made |
{'RUNNING_MODAL'} | Operator is entering modal mode (from invoke()) |
{'PASS_THROUGH'} | Modal: allow other operators to also handle this event |
{'INTERFACE'} | Operator handled event but did not execute (popup shown) |
---
Operator Method Signatures
class bpy.types.Operator:
bl_idname: str # "category.name" — REQUIRED
bl_label: str # Display name — REQUIRED
bl_description: str # Tooltip (overrides docstring)
bl_options: set[str] # {'REGISTER', 'UNDO', ...}
bl_translation_context: str # i18n context
bl_undo_group: str # Group name for undo grouping
@classmethod
def poll(cls, context) -> bool: ...
def execute(self, context) -> set[str]: ...
def invoke(self, context, event) -> set[str]: ...
def modal(self, context, event) -> set[str]: ...
def draw(self, context) -> None: ...
def cancel(self, context) -> None: ...
def report(self, type: set[str], message: str) -> None: ...
# Access layout for draw()
layout: bpy.types.UILayout # Available in draw()Event Object (used in invoke/modal)
event.type # str: 'LEFTMOUSE', 'RIGHTMOUSE', 'ESC', 'TIMER', 'A', 'B', etc.
event.value # str: 'PRESS', 'RELEASE', 'CLICK', 'DOUBLE_CLICK', 'NOTHING'
event.mouse_x # int: absolute mouse X position
event.mouse_y # int: absolute mouse Y position
event.mouse_region_x # int: mouse X relative to region
event.mouse_region_y # int: mouse Y relative to region
event.shift # bool: Shift held
event.ctrl # bool: Ctrl held
event.alt # bool: Alt held
event.oskey # bool: OS/Super key held---
Reference Links
- references/methods.md — Complete API signatures for Operator, WindowManager, Event, and registration functions
- references/examples.md — Working code examples for all operator patterns
- references/anti-patterns.md — Common mistakes when writing operators, with fixes
Official Sources
- https://docs.blender.org/api/current/bpy.types.Operator.html
- https://docs.blender.org/api/current/bpy.ops.html
- https://docs.blender.org/api/current/bpy.types.Event.html
- https://developer.blender.org/docs/release_notes/4.0/python_api/
Related Skills
- blender-core-api — bpy module structure, context system, data access patterns
blender-syntax-operators — Anti-Patterns
Common mistakes when creating Blender operators, with explanations and fixes.
---
AP-01: Using Dict Context Override in Blender 4.0+
Severity: BREAKING — code will crash
# WRONG — REMOVED in Blender 4.0
override = bpy.context.copy()
override['active_object'] = obj
bpy.ops.object.modifier_apply(override, modifier="Subsurf")
# TypeError: Converting py args to operator properties: ...
# CORRECT — Blender 3.2+ / 4.0+ / 5.x
with bpy.context.temp_override(active_object=obj, object=obj):
bpy.ops.object.modifier_apply(modifier="Subsurf")Why: Blender 4.0 removed the dict-based context override mechanism entirely. The temp_override() context manager was introduced in 3.2 as the replacement and became REQUIRED in 4.0.
---
AP-02: Forgetting @classmethod on poll()
Severity: BREAKING — registration fails
# WRONG — missing @classmethod
class MY_OT_broken(bpy.types.Operator):
bl_idname = "my.broken"
bl_label = "Broken"
def poll(self, context): # Takes 'self' instead of 'cls'
return True
# TypeError during registration or unexpected behavior
# CORRECT
class MY_OT_fixed(bpy.types.Operator):
bl_idname = "my.fixed"
bl_label = "Fixed"
@classmethod
def poll(cls, context):
return TrueWhy: Blender calls poll() on the class, not on an instance. Without @classmethod, Python passes an unexpected first argument.
---
AP-03: Returning None or String from execute/invoke/modal
Severity: BREAKING — crashes Blender or produces RuntimeError
# WRONG — returns None
def execute(self, context):
context.active_object.location.z += 1.0
# Forgot return statement
# WRONG — returns a string instead of a set
def execute(self, context):
return 'FINISHED' # String, not a set
# CORRECT
def execute(self, context):
context.active_object.location.z += 1.0
return {'FINISHED'}Why: Blender expects a set of strings as the return value. Returning None or a plain string causes a TypeError or undefined behavior.
---
AP-04: Uppercase Characters in bl_idname
Severity: BREAKING — registration fails in Blender 4.0+
# WRONG — uppercase letters in bl_idname
class MY_OT_bad(bpy.types.Operator):
bl_idname = "My.BadOperator" # FAILS registration
# WRONG — mixed case
class MY_OT_bad2(bpy.types.Operator):
bl_idname = "myTools.doSomething" # FAILS in 4.0+
# CORRECT — all lowercase with underscores
class MY_OT_good(bpy.types.Operator):
bl_idname = "my.good_operator"Why: bl_idname MUST be "category.operator_name" — both parts lowercase, separated by a single dot. Blender 4.0+ strictly enforces this.
---
AP-05: Not Handling Cancellation in Modal Operators
Severity: HIGH — user gets stuck in modal with no escape
# WRONG — no ESC/RIGHTMOUSE handling
def modal(self, context, event):
if event.type == 'LEFTMOUSE':
return {'FINISHED'}
return {'RUNNING_MODAL'}
# User CANNOT cancel this operator — stuck forever
# CORRECT — always handle cancellation
def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
return {'FINISHED'}
return {'PASS_THROUGH'}Why: Modal operators intercept ALL events. Without an escape mechanism, the user is locked into the modal state with no way to cancel.
---
AP-06: Not Cleaning Up Timer in Modal cancel()
Severity: HIGH — memory leak, timer keeps firing after operator ends
# WRONG — timer created but never removed
class MY_OT_leaked_timer(bpy.types.Operator):
bl_idname = "my.leaked_timer"
bl_label = "Leaked Timer"
_timer = None
def invoke(self, context, event):
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
if event.type == 'ESC':
return {'CANCELLED'} # Timer still running!
return {'PASS_THROUGH'}
# CORRECT — cleanup in both cancel() and finish paths
class MY_OT_clean_timer(bpy.types.Operator):
bl_idname = "my.clean_timer"
bl_label = "Clean Timer"
_timer = None
def invoke(self, context, event):
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
if event.type == 'ESC':
self.cancel(context)
return {'CANCELLED'}
if event.type == 'LEFTMOUSE':
self.cancel(context) # Also clean up on finish
return {'FINISHED'}
return {'PASS_THROUGH'}
def cancel(self, context):
if self._timer is not None:
context.window_manager.event_timer_remove(self._timer)
self._timer = NoneWhy: Timers are global resources managed by the WindowManager. If not explicitly removed, they continue generating events indefinitely, consuming resources and potentially causing errors.
---
AP-07: Calling bpy.ops Inside Panel.draw()
Severity: HIGH — causes RuntimeError or undefined behavior
# WRONG — calling operator in draw callback
class MY_PT_bad_panel(bpy.types.Panel):
bl_label = "Bad Panel"
bl_idname = "MY_PT_bad_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
def draw(self, context):
if context.active_object:
bpy.ops.object.shade_smooth() # FORBIDDEN — draw is read-only
self.layout.label(text="Panel")
# CORRECT — use layout.operator() to create a button
class MY_PT_good_panel(bpy.types.Panel):
bl_label = "Good Panel"
bl_idname = "MY_PT_good_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
def draw(self, context):
self.layout.operator("object.shade_smooth", text="Smooth Shading")Why: Panel.draw() is called on every UI redraw and runs in a restricted context. Calling operators or modifying data causes errors, infinite loops, or crashes.
---
AP-08: Calling Operator Without Checking poll()
Severity: MEDIUM — RuntimeError at runtime
# WRONG — blindly calling operator
bpy.ops.object.mode_set(mode='EDIT')
# RuntimeError if no active object or already in edit mode
# CORRECT — check poll first
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='EDIT')
else:
print("Cannot switch to edit mode in current context")Why: Every operator has a poll() method that checks preconditions. Calling an operator when its poll fails raises RuntimeError: Operator bpy.ops.X.poll() failed, context is incorrect.
---
AP-09: Using modal_handler_add Without Returning RUNNING_MODAL
Severity: BREAKING — modal never activates or crashes
# WRONG — returns FINISHED after adding modal handler
def invoke(self, context, event):
context.window_manager.modal_handler_add(self)
return {'FINISHED'} # Operator finishes immediately, modal() never called
# WRONG — returns RUNNING_MODAL without adding modal handler
def invoke(self, context, event):
return {'RUNNING_MODAL'} # No handler registered, events go nowhere
# CORRECT — add handler THEN return RUNNING_MODAL
def invoke(self, context, event):
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}Why: modal_handler_add() registers the operator for events, and {'RUNNING_MODAL'} tells Blender to keep the operator alive. Both are required — without the handler, events are lost; without RUNNING_MODAL, the operator terminates.
---
AP-10: Using Assignment Instead of Annotation for Properties
Severity: BREAKING — properties silently ignored
# WRONG — assignment syntax (=)
class MY_OT_broken_props(bpy.types.Operator):
bl_idname = "my.broken_props"
bl_label = "Broken"
size = bpy.props.FloatProperty(name="Size", default=1.0)
# Property is a class variable, NOT registered with Blender's RNA system
# self.size returns the property descriptor, not a float value
# CORRECT — annotation syntax (:)
class MY_OT_working_props(bpy.types.Operator):
bl_idname = "my.working_props"
bl_label = "Working"
size: bpy.props.FloatProperty(name="Size", default=1.0)
# Property is properly registered, self.size returns the float valueWhy: Blender uses Python annotations (:) to detect properties for RNA registration. Assignment (=) creates a regular class attribute that Blender ignores — the property won't appear in the UI and self.size will return the property descriptor object instead of the value.
---
AP-11: Missing bl_options UNDO for Data-Modifying Operators
Severity: MEDIUM — user cannot undo the operation
# WRONG — modifies data without UNDO flag
class MY_OT_no_undo(bpy.types.Operator):
bl_idname = "my.no_undo"
bl_label = "No Undo"
bl_options = {'REGISTER'} # Missing 'UNDO'
def execute(self, context):
bpy.ops.mesh.primitive_cube_add() # Creates object with no undo step
return {'FINISHED'}
# CORRECT
class MY_OT_with_undo(bpy.types.Operator):
bl_idname = "my.with_undo"
bl_label = "With Undo"
bl_options = {'REGISTER', 'UNDO'} # User can Ctrl+Z
def execute(self, context):
bpy.ops.mesh.primitive_cube_add()
return {'FINISHED'}Why: Without 'UNDO' in bl_options, Blender does not create an undo step when the operator finishes. Users expect Ctrl+Z to work on any operation that changes the scene.
---
AP-12: Storing State as Class Attributes Shared Across Instances
Severity: MEDIUM — subtle bugs with shared mutable state
# WRONG — mutable class-level state shared across all invocations
class MY_OT_shared_state(bpy.types.Operator):
bl_idname = "my.shared_state"
bl_label = "Shared State"
collected_objects = [] # Class attribute — shared across ALL instances
def execute(self, context):
self.collected_objects.append(context.active_object)
# This list persists and grows across multiple operator calls
return {'FINISHED'}
# CORRECT — initialize per-invocation state in invoke() or execute()
class MY_OT_instance_state(bpy.types.Operator):
bl_idname = "my.instance_state"
bl_label = "Instance State"
def execute(self, context):
collected = [] # Local variable — fresh each call
collected.append(context.active_object)
return {'FINISHED'}
# For modal operators, use instance attributes set in invoke():
class MY_OT_modal_state(bpy.types.Operator):
bl_idname = "my.modal_state"
bl_label = "Modal State"
_items: list = None # Type hint only, initialized in invoke
def invoke(self, context, event):
self._items = [] # Fresh list per invocation
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}Why: Python class attributes are shared across all instances. For operators, Blender may reuse instances or create new ones unpredictably. Mutable class-level state (lists, dicts) accumulates data across calls, leading to subtle bugs.
---
AP-13: Forgetting to Unregister Menu Functions
Severity: LOW — duplicate menu entries accumulate
# WRONG — registers menu but never unregisters
def register():
bpy.utils.register_class(MY_OT_tool)
bpy.types.VIEW3D_MT_object.append(menu_func)
def unregister():
bpy.utils.unregister_class(MY_OT_tool)
# Forgot to remove menu_func — entry duplicates on addon reload
# CORRECT
def unregister():
bpy.types.VIEW3D_MT_object.remove(menu_func)
bpy.utils.unregister_class(MY_OT_tool)Why: Menu append functions are stored in a list. If not removed on unregister, reloading the addon appends the function again, creating duplicate menu entries.
---
AP-14: Running Viewport Operators Without Area Context
Severity: MEDIUM — RuntimeError in scripts and background mode
# WRONG — calling viewport operator without VIEW_3D context
bpy.ops.view3d.snap_cursor_to_center()
# RuntimeError: Operator bpy.ops.view3d.snap_cursor_to_center.poll() failed
# CORRECT — provide viewport context
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
with bpy.context.temp_override(window=window, area=area):
bpy.ops.view3d.snap_cursor_to_center()
breakWhy: Operators in the view3d namespace require a VIEW_3D area in the context. Scripts, timers, and background mode do not have this by default.
---
AP-15: Using invoke_props_dialog Without draw()
Severity: LOW — dialog shows auto-generated layout (may be acceptable)
# SUBOPTIMAL — no draw(), dialog auto-generates from properties
class MY_OT_auto_layout(bpy.types.Operator):
bl_idname = "my.auto_layout"
bl_label = "Auto Layout"
name: bpy.props.StringProperty(name="Name")
count: bpy.props.IntProperty(name="Count")
mode: bpy.props.EnumProperty(
name="Mode",
items=[('A', "A", ""), ('B', "B", "")],
)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
return {'FINISHED'}
# Auto-generated layout shows ALL properties in declaration order
# This is acceptable for simple dialogs
# BETTER — custom draw() for complex dialogs
class MY_OT_custom_layout(bpy.types.Operator):
bl_idname = "my.custom_layout"
bl_label = "Custom Layout"
name: bpy.props.StringProperty(name="Name")
count: bpy.props.IntProperty(name="Count")
mode: bpy.props.EnumProperty(
name="Mode",
items=[('A', "A", ""), ('B', "B", "")],
)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "name")
row = layout.row()
row.prop(self, "mode", expand=True) # Radio buttons
layout.prop(self, "count")
def execute(self, context):
return {'FINISHED'}Why: Without draw(), Blender auto-generates the dialog layout from all properties in declaration order. For simple operators this is fine, but for complex dialogs with conditional fields or custom layouts, implementing draw() provides better UX.
---
Sources
- https://docs.blender.org/api/current/bpy.types.Operator.html
- https://docs.blender.org/api/current/info_gotcha.html
- https://developer.blender.org/docs/release_notes/4.0/python_api/
- https://devtalk.blender.org/t/deprecationwarning-passing-in-context-overrides-is-deprecated/27870
blender-syntax-operators — Working Examples
All examples verified against Blender 4.x API. Version-specific notes included where applicable.
---
Example 1: Complete Addon with Operator
Minimal addon structure with a single operator, menu entry, and proper registration.
# Blender 3.x/4.x/5.x
bl_info = {
"name": "My Custom Tools",
"author": "Developer",
"version": (1, 0, 0),
"blender": (4, 0, 0),
"location": "View3D > Object > My Custom Tools",
"description": "Example addon with a custom operator",
"category": "Object",
}
# NOTE: In Blender 4.2+ extensions, use blender_manifest.toml instead of bl_info
import bpy
class MYCTOOLS_OT_duplicate_linked(bpy.types.Operator):
"""Duplicate selected objects as linked copies"""
bl_idname = "myctools.duplicate_linked"
bl_label = "Duplicate Linked"
bl_options = {'REGISTER', 'UNDO'}
offset: bpy.props.FloatVectorProperty(
name="Offset",
default=(2.0, 0.0, 0.0),
subtype='TRANSLATION',
)
@classmethod
def poll(cls, context):
return (
context.mode == 'OBJECT'
and len(context.selected_objects) > 0
)
def execute(self, context):
original_selection = context.selected_objects[:]
for obj in original_selection:
new_obj = obj.copy() # Linked copy (shares mesh data)
new_obj.location = obj.location.copy()
new_obj.location.x += self.offset[0]
new_obj.location.y += self.offset[1]
new_obj.location.z += self.offset[2]
context.collection.objects.link(new_obj)
self.report({'INFO'}, f"Duplicated {len(original_selection)} objects")
return {'FINISHED'}
def menu_func(self, context):
self.layout.operator(MYCTOOLS_OT_duplicate_linked.bl_idname,
icon='DUPLICATE')
def register():
bpy.utils.register_class(MYCTOOLS_OT_duplicate_linked)
bpy.types.VIEW3D_MT_object.append(menu_func)
def unregister():
bpy.types.VIEW3D_MT_object.remove(menu_func)
bpy.utils.unregister_class(MYCTOOLS_OT_duplicate_linked)
if __name__ == "__main__":
register()---
Example 2: Modal Operator — Interactive Move Along Normal
Modal operator that lets the user interactively move an object along its local Z axis by moving the mouse.
# Blender 3.x/4.x/5.x
import bpy
from mathutils import Vector
class MYTOOLS_OT_move_along_normal(bpy.types.Operator):
"""Move active object along its local Z axis with mouse"""
bl_idname = "mytools.move_along_normal"
bl_label = "Move Along Normal"
bl_options = {'REGISTER', 'UNDO', 'GRAB_CURSOR_Y'}
offset: bpy.props.FloatProperty(
name="Offset",
default=0.0,
)
_initial_mouse_y: int = 0
_initial_location: Vector = None
@classmethod
def poll(cls, context):
return context.active_object is not None and context.mode == 'OBJECT'
def invoke(self, context, event):
self._initial_mouse_y = event.mouse_y
self._initial_location = context.active_object.location.copy()
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
if event.type == 'MOUSEMOVE':
delta = (event.mouse_y - self._initial_mouse_y) * 0.01
self.offset = delta
obj = context.active_object
local_z = obj.matrix_world.to_3x3() @ Vector((0, 0, 1))
obj.location = self._initial_location + local_z * delta
context.area.header_text_set(f"Offset: {delta:.3f}")
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
context.area.header_text_set(None) # Restore header
return {'FINISHED'}
elif event.type in {'RIGHTMOUSE', 'ESC'}:
context.active_object.location = self._initial_location
context.area.header_text_set(None)
return {'CANCELLED'}
return {'RUNNING_MODAL'}---
Example 3: Operator with File Browser
Operator that opens a file browser for the user to select a file.
# Blender 3.x/4.x/5.x
import bpy
import os
class MYTOOLS_OT_import_csv(bpy.types.Operator):
"""Import data from a CSV file"""
bl_idname = "mytools.import_csv"
bl_label = "Import CSV"
bl_options = {'REGISTER', 'UNDO'}
filepath: bpy.props.StringProperty(
name="File Path",
subtype='FILE_PATH',
)
filter_glob: bpy.props.StringProperty(
default="*.csv",
options={'HIDDEN'},
)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
if not os.path.exists(self.filepath):
self.report({'ERROR'}, f"File not found: {self.filepath}")
return {'CANCELLED'}
with open(self.filepath, 'r') as f:
lines = f.readlines()
self.report({'INFO'}, f"Read {len(lines)} lines from {self.filepath}")
return {'FINISHED'}
# Register in Import menu
def menu_func_import(self, context):
self.layout.operator(MYTOOLS_OT_import_csv.bl_idname, text="CSV (.csv)")
def register():
bpy.utils.register_class(MYTOOLS_OT_import_csv)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
bpy.utils.unregister_class(MYTOOLS_OT_import_csv)---
Example 4: Modal Timer Operator
Operator that executes periodic work using a timer.
# Blender 3.x/4.x/5.x
import bpy
class MYTOOLS_OT_auto_save(bpy.types.Operator):
"""Auto-save the file at regular intervals"""
bl_idname = "mytools.auto_save"
bl_label = "Auto Save Timer"
bl_options = {'REGISTER'}
interval: bpy.props.FloatProperty(
name="Interval (seconds)",
default=60.0,
min=5.0,
)
_timer = None
_count = 0
def modal(self, context, event):
if event.type in {'ESC'}:
self.cancel(context)
self.report({'INFO'}, "Auto-save stopped")
return {'CANCELLED'}
if event.type == 'TIMER':
self._count += 1
if bpy.data.is_saved:
bpy.ops.wm.save_mainfile()
self.report({'INFO'}, f"Auto-saved ({self._count})")
return {'PASS_THROUGH'}
def invoke(self, context, event):
self._timer = context.window_manager.event_timer_add(
self.interval, window=context.window
)
context.window_manager.modal_handler_add(self)
self.report({'INFO'}, f"Auto-save started (every {self.interval}s)")
return {'RUNNING_MODAL'}
def cancel(self, context):
if self._timer is not None:
context.window_manager.event_timer_remove(self._timer)
self._timer = None---
Example 5: Operator with Enum Property and Draw Override
# Blender 3.x/4.x/5.x
import bpy
class MYTOOLS_OT_create_primitive(bpy.types.Operator):
"""Create a primitive mesh object with options"""
bl_idname = "mytools.create_primitive"
bl_label = "Create Primitive"
bl_options = {'REGISTER', 'UNDO'}
primitive_type: bpy.props.EnumProperty(
name="Type",
items=[
('CUBE', "Cube", "Create a cube", 'MESH_CUBE', 0),
('SPHERE', "Sphere", "Create a UV sphere", 'MESH_UVSPHERE', 1),
('CYLINDER', "Cylinder", "Create a cylinder", 'MESH_CYLINDER', 2),
('CONE', "Cone", "Create a cone", 'MESH_CONE', 3),
],
default='CUBE',
)
size: bpy.props.FloatProperty(name="Size", default=1.0, min=0.01)
location_offset: bpy.props.FloatVectorProperty(
name="Location",
default=(0.0, 0.0, 0.0),
subtype='TRANSLATION',
)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
layout.prop(self, "primitive_type")
layout.prop(self, "size")
layout.prop(self, "location_offset")
def execute(self, context):
ops_map = {
'CUBE': lambda: bpy.ops.mesh.primitive_cube_add(
size=self.size, location=self.location_offset),
'SPHERE': lambda: bpy.ops.mesh.primitive_uv_sphere_add(
radius=self.size / 2, location=self.location_offset),
'CYLINDER': lambda: bpy.ops.mesh.primitive_cylinder_add(
radius=self.size / 2, location=self.location_offset),
'CONE': lambda: bpy.ops.mesh.primitive_cone_add(
radius1=self.size / 2, location=self.location_offset),
}
ops_map[self.primitive_type]()
self.report({'INFO'}, f"Created {self.primitive_type.lower()}")
return {'FINISHED'}---
Example 6: Context Override for Batch Operations (Blender 4.0+)
# Blender 4.0+ ONLY — context.temp_override is REQUIRED
import bpy
def apply_all_modifiers(obj):
"""Apply all modifiers on an object using context override."""
# MUST override active_object because modifier_apply checks context
with bpy.context.temp_override(active_object=obj, object=obj):
for mod in obj.modifiers[:]: # Copy list — modifiers are removed during iteration
try:
bpy.ops.object.modifier_apply(modifier=mod.name)
except RuntimeError as e:
print(f"Cannot apply {mod.name}: {e}")
def batch_shade_smooth(objects):
"""Apply smooth shading to a list of objects."""
for obj in objects:
with bpy.context.temp_override(
active_object=obj,
selected_objects=[obj],
object=obj,
):
bpy.ops.object.shade_smooth()
# Usage:
# apply_all_modifiers(bpy.context.active_object)
# batch_shade_smooth(bpy.context.selected_objects)---
Example 7: Operator with Multiple Return Paths
Demonstrates proper use of different return values.
# Blender 3.x/4.x/5.x
import bpy
class MYTOOLS_OT_safe_delete(bpy.types.Operator):
"""Delete active object with safety checks"""
bl_idname = "mytools.safe_delete"
bl_label = "Safe Delete"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None
def invoke(self, context, event):
obj = context.active_object
# Check for protected objects
if obj.get("protected"):
self.report({'WARNING'}, f"'{obj.name}' is protected")
return {'CANCELLED'}
# Check for objects with children
if obj.children:
return context.window_manager.invoke_confirm(self, event)
# Simple case — execute directly
return self.execute(context)
def execute(self, context):
obj = context.active_object
name = obj.name
bpy.data.objects.remove(obj, do_unlink=True)
self.report({'INFO'}, f"Deleted '{name}'")
return {'FINISHED'}---
Example 8: Viewport Operator with Area Override
Running a viewport-dependent operator from a script or timer.
# Blender 4.0+
import bpy
def find_view3d_context():
"""Find a VIEW_3D area and return override kwargs."""
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
return {
'window': window,
'area': area,
'region': region,
}
return None
def snap_cursor_to_selected():
"""Snap 3D cursor to selected objects, handling missing viewport."""
ctx = find_view3d_context()
if ctx is None:
print("No 3D Viewport found")
return False
with bpy.context.temp_override(**ctx):
bpy.ops.view3d.snap_cursor_to_selected()
return True---
Example 9: Macro Operator (Combining Operators)
# Blender 3.x/4.x/5.x
import bpy
class MYTOOLS_OT_duplicate_and_move(bpy.types.Macro):
"""Duplicate object and move it"""
bl_idname = "mytools.duplicate_and_move"
bl_label = "Duplicate and Move"
bl_options = {'REGISTER', 'UNDO'}
def register():
bpy.utils.register_class(MYTOOLS_OT_duplicate_and_move)
# Define the macro steps
MYTOOLS_OT_duplicate_and_move.define("OBJECT_OT_duplicate")
MYTOOLS_OT_duplicate_and_move.define("TRANSFORM_OT_translate")
def unregister():
bpy.utils.unregister_class(MYTOOLS_OT_duplicate_and_move)---
Example 10: Registering Multiple Operators with Factory
# Blender 3.x/4.x/5.x
import bpy
class MYTOOLS_OT_action_a(bpy.types.Operator):
bl_idname = "mytools.action_a"
bl_label = "Action A"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
self.report({'INFO'}, "Action A executed")
return {'FINISHED'}
class MYTOOLS_OT_action_b(bpy.types.Operator):
bl_idname = "mytools.action_b"
bl_label = "Action B"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
self.report({'INFO'}, "Action B executed")
return {'FINISHED'}
class MYTOOLS_PT_tools_panel(bpy.types.Panel):
bl_label = "My Tools"
bl_idname = "MYTOOLS_PT_tools_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "My Tools"
def draw(self, context):
layout = self.layout
layout.operator("mytools.action_a")
layout.operator("mytools.action_b")
# Factory registration — handles order automatically
classes = (
MYTOOLS_OT_action_a,
MYTOOLS_OT_action_b,
MYTOOLS_PT_tools_panel,
)
register, unregister = bpy.utils.register_classes_factory(classes)---
Sources
- https://docs.blender.org/api/current/bpy.types.Operator.html
- https://docs.blender.org/api/current/info_tutorial_addon.html
- https://docs.blender.org/api/current/bpy.types.Macro.html
- https://docs.blender.org/api/current/bpy.ops.html
blender-syntax-operators — Method Reference
Complete API signatures for Blender operator creation, registration, and invocation.
---
bpy.types.Operator — Class Attributes
| Attribute | Type | Required | Description |
|---|---|---|---|
bl_idname | str | REQUIRED | Unique identifier. Format: "category.operator_name" (lowercase only) |
bl_label | str | REQUIRED | Display name shown in menus and buttons |
bl_description | str | Optional | Tooltip text. Overrides class docstring if set |
bl_options | set[str] | Optional | Set of option flags. Default: {'REGISTER'} |
bl_translation_context | str | Optional | Translation context for i18n |
bl_undo_group | str | Optional | Name for grouping undo steps |
---
bpy.types.Operator — Instance Methods
poll(cls, context) -> bool
@classmethod
def poll(cls, context):
"""Determine if operator can execute in the current context.
Args:
cls: The operator class (NOT instance — this is a classmethod).
context: bpy.types.Context — current Blender context.
Returns:
bool: True if operator can run. False disables the button in UI
and raises RuntimeError if called from script.
Rules:
- ALWAYS decorate with @classmethod
- NEVER modify any data in poll() — it is called frequently for UI updates
- NEVER raise exceptions — return False instead
- Keep poll() fast — it runs on every UI redraw
"""execute(self, context) -> set[str]
def execute(self, context):
"""Main operator logic. Called after invoke() or directly.
Args:
self: Operator instance. Access properties via self.property_name.
context: bpy.types.Context — current Blender context.
Returns:
set[str]: MUST be one of:
{'FINISHED'} — operation completed successfully
{'CANCELLED'} — operation was cancelled
{'RUNNING_MODAL'} — entering modal mode (rare from execute)
Rules:
- ALWAYS return a set, NEVER return None or a plain string
- Access operator properties via self (e.g., self.size)
- Use self.report() to communicate results to the user
"""invoke(self, context, event) -> set[str]
def invoke(self, context, event):
"""Called when user triggers the operator (button, menu, shortcut).
Args:
self: Operator instance.
context: bpy.types.Context.
event: bpy.types.Event — mouse position, modifier keys, etc.
Returns:
set[str]: MUST be one of:
{'FINISHED'} — done immediately
{'CANCELLED'} — abort
{'RUNNING_MODAL'} — entering modal mode
{'INTERFACE'} — UI shown but not executed yet
Common patterns:
return self.execute(context) # Direct execution
return context.window_manager.invoke_props_dialog(self) # Show dialog
return context.window_manager.invoke_confirm(self, event) # Confirm popup
context.window_manager.modal_handler_add(self) # Enter modal
return {'RUNNING_MODAL'}
Rules:
- If invoke() is not defined, Blender calls execute() directly
- Store event data needed later (e.g., self._init_mouse = event.mouse_x)
- For modal: MUST call modal_handler_add() before returning RUNNING_MODAL
"""modal(self, context, event) -> set[str]
def modal(self, context, event):
"""Called repeatedly for each event while operator is modal.
Args:
self: Operator instance.
context: bpy.types.Context.
event: bpy.types.Event — the current event to process.
Returns:
set[str]: MUST be one of:
{'RUNNING_MODAL'} — continue receiving events
{'FINISHED'} — done, stop modal
{'CANCELLED'} — abort, stop modal
{'PASS_THROUGH'} — let other handlers also process this event
Rules:
- ALWAYS handle ESC/RIGHTMOUSE to allow user cancellation
- ALWAYS clean up resources (timers, draw handlers) on finish/cancel
- Return PASS_THROUGH for events you don't handle
- Avoid heavy computation — modal() is called per-event
"""draw(self, context) -> None
def draw(self, context):
"""Draw the operator's properties in a dialog or redo panel (F9).
Args:
self: Operator instance. self.layout is the UILayout.
context: bpy.types.Context.
Returns:
None
Rules:
- NEVER modify data in draw() — it is a read-only callback
- Use self.layout.prop(self, "property_name") to draw properties
- If not defined, Blender auto-generates layout from properties
"""cancel(self, context) -> None
def cancel(self, context):
"""Called when a modal operator is cancelled (ESC, RIGHTMOUSE, or system).
Args:
self: Operator instance.
context: bpy.types.Context.
Returns:
None
Rules:
- ALWAYS remove timers: context.window_manager.event_timer_remove(self._timer)
- ALWAYS remove draw handlers if added
- Restore any temporary state changes
"""report(self, type, message) -> None
def report(self, type, message):
"""Display a message to the user in the status bar / info editor.
Args:
type: set[str] — one of:
{'DEBUG'} — debug output (not shown to user)
{'INFO'} — information message (blue)
{'OPERATOR'} — operator log
{'WARNING'} — warning message (yellow)
{'ERROR'} — error message (red, does NOT raise exception)
{'ERROR_INVALID_INPUT'} — invalid input error
message: str — the message text.
Rules:
- type MUST be a set (e.g., {'INFO'}), not a string
- {'ERROR'} does NOT raise a Python exception — the operator continues
- Use return {'CANCELLED'} after reporting an error to stop execution
"""---
bpy.types.Event — Properties
| Property | Type | Description |
|---|---|---|
type | str | Event type: 'LEFTMOUSE', 'RIGHTMOUSE', 'MIDDLEMOUSE', 'MOUSEMOVE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'ESC', 'RET', 'SPACE', 'TIMER', 'A'..'Z', 'ZERO'..'NINE', 'F1'..'F19' |
value | str | 'PRESS', 'RELEASE', 'CLICK', 'DOUBLE_CLICK', 'CLICK_DRAG', 'NOTHING' |
mouse_x | int | Absolute mouse X position (window coordinates) |
mouse_y | int | Absolute mouse Y position (window coordinates) |
mouse_region_x | int | Mouse X position relative to current region |
mouse_region_y | int | Mouse Y position relative to current region |
mouse_prev_x | int | Previous absolute mouse X |
mouse_prev_y | int | Previous absolute mouse Y |
pressure | float | Tablet pressure (0.0 to 1.0) |
tilt | tuple[float, float] | Tablet tilt (X, Y) |
shift | bool | Shift key held |
ctrl | bool | Ctrl key held |
alt | bool | Alt key held |
oskey | bool | OS/Super/Windows key held |
is_tablet | bool | Event from tablet device |
is_mouse_absolute | bool | Mouse position is in absolute screen coordinates |
---
WindowManager — Operator Invocation Methods
invoke_props_dialog(operator, width=300) -> set[str]
context.window_manager.invoke_props_dialog(self, width=300)Shows a dialog with the operator's properties. When user clicks OK, execute() is called. Returns {'RUNNING_MODAL'} internally.
Args:
operator: The operator instance (useself)width: Dialog width in pixels (default 300)
invoke_props_popup(operator, event) -> set[str]
context.window_manager.invoke_props_popup(self, event)Shows a popup with properties. Calls execute() immediately AND when properties change. Useful for interactive adjustment.
invoke_confirm(operator, event) -> set[str]
context.window_manager.invoke_confirm(self, event)Shows "OK?" confirmation dialog. Calls execute() on confirm.
invoke_popup(operator, width=300) -> set[str]
context.window_manager.invoke_popup(self, width=300)Shows a popup that only calls draw() — does NOT call execute() on close. Useful for info popups.
modal_handler_add(operator) -> bool
context.window_manager.modal_handler_add(self)Registers the operator to receive modal events. MUST be called in invoke() before returning {'RUNNING_MODAL'}. Returns True on success.
event_timer_add(time_step, window) -> Timer
timer = context.window_manager.event_timer_add(0.1, window=context.window)Creates a timer that generates 'TIMER' events at the specified interval (seconds). Store the returned timer for removal.
event_timer_remove(timer) -> None
context.window_manager.event_timer_remove(self._timer)Removes a previously created timer. ALWAYS call in cancel() and on {'FINISHED'}.
---
Registration Functions
bpy.utils.register_class(cls)
bpy.utils.register_class(MYCAT_OT_simple_action)Registers a class with Blender's RNA system. The class MUST be a subclass of a bpy.types type (Operator, Panel, Menu, PropertyGroup, etc.).
Rules:
- Registration order matters: register dependencies (PropertyGroup) BEFORE dependents (Operator that uses them)
- Raises
ValueErrorif class is already registered - Raises
TypeErrorifbl_idnameformat is invalid
bpy.utils.unregister_class(cls)
bpy.utils.unregister_class(MYCAT_OT_simple_action)Unregisters a class. ALWAYS unregister in reverse order of registration.
bpy.utils.register_classes_factory(classes)
classes = (MYCAT_OT_action_one, MYCAT_OT_action_two, MYCAT_PT_panel)
register, unregister = bpy.utils.register_classes_factory(classes)Returns a (register, unregister) tuple that handles all classes. Registration order follows tuple order; unregistration is reversed.
---
bpy.ops Invocation
Calling Operators
# Standard call
bpy.ops.mesh.primitive_cube_add(size=2.0)
# With poll check
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='EDIT')
# Returns: {'FINISHED'}, {'CANCELLED'}, or {'RUNNING_MODAL'}
result = bpy.ops.object.select_all(action='SELECT')context.temp_override() (Blender 3.2+, REQUIRED in 4.0+)
# Signature
bpy.context.temp_override(
window=None, # bpy.types.Window
area=None, # bpy.types.Area
region=None, # bpy.types.Region
**kw # Any context attribute (active_object, selected_objects, etc.)
)Rules:
- Region MUST belong to the specified (or current) area
- Area MUST belong to the specified (or current) window
- Cannot switch to/from fullscreen areas
- NEVER use dict-based overrides in Blender 4.0+ — they are REMOVED
---
Operator Properties (bpy.props on operators)
Operator properties are defined as class annotations and appear in the F9 redo panel and invoke dialogs.
class MY_OT_example(bpy.types.Operator):
bl_idname = "my.example"
bl_label = "Example"
# Properties available via self.name, self.count, etc.
name: bpy.props.StringProperty(name="Name", default="")
count: bpy.props.IntProperty(name="Count", default=1, min=0)
size: bpy.props.FloatProperty(name="Size", default=1.0)
enabled: bpy.props.BoolProperty(name="Enabled", default=True)
mode: bpy.props.EnumProperty(
name="Mode",
items=[
('ADD', "Add", "Add mode"),
('REMOVE', "Remove", "Remove mode"),
],
)Rules:
- Properties MUST use annotation syntax (
:) not assignment (=) - Properties are passed as keyword arguments when calling operator from script:
bpy.ops.my.example(count=5) - Properties persist in the F9 redo panel until the next operator runs
'SKIP_SAVE'in propertyoptionsprevents the value from being saved in presets/redo
---
Sources
- https://docs.blender.org/api/current/bpy.types.Operator.html
- https://docs.blender.org/api/current/bpy.types.Event.html
- https://docs.blender.org/api/current/bpy.ops.html
- https://docs.blender.org/api/current/bpy.types.WindowManager.html
- https://docs.blender.org/api/current/bpy.utils.html
- https://docs.blender.org/api/current/bpy.props.html