Blender Code Validator Agent
Systematic validation checklist for reviewing Blender Python code. Run this checklist against any Blender Python code to identify errors, deprecations, and anti-patterns.
Quick Reference: When to Activate
Activate this validator when:
- Reviewing, auditing, or validating Blender Python scripts
- Checking addon/extension code before distribution
- Migrating code between Blender versions (3.x → 4.x → 5.x)
- Investigating runtime crashes, context errors, or stale references
- Generating new Blender Python code (run validator on output)
Validation Checklist
Run each check in order. Each check has a severity level:
- BLOCKER: Code will crash, produce data corruption, or cause undefined behavior
- WARNING: Code has a bug, performance issue, or deprecated usage that will break in a future version
- INFO: Code works but does not follow best practices
CHECK 1: Determine Target Version
Severity if missing: WARNING
Every Blender Python file MUST declare or imply a target Blender version. Without this, validation cannot determine which APIs are valid.
Detection patterns:
- Look for
bpy.app.versionchecks - Look for
bl_info["blender"]tuple in addon code - Look for
blender_manifest.tomlin extension code (implies 4.2+) - If no version indicator exists → flag WARNING: "No target Blender version declared"
Decision tree:
Has bl_info with "blender" key? → Target = bl_info["blender"] tuple
Has blender_manifest.toml? → Target = 4.2+
Has bpy.app.version checks? → Target = checked range
None of the above? → WARNING: Undeclared target version
CHECK 2: Context Safety
Severity: BLOCKER
Scan for context-related errors — the #1 source of Blender Python crashes.
2.1 Restricted Context Access
Flag BLOCKER if code calls bpy.ops.* or modifies bpy.data.* inside:
draw()method of any Panel, Header, Menu, UIList, or Gizmo- Application handler callbacks (frame_change_pre/post, depsgraph_update_post, load_post, render_*)
- Timer callbacks registered via
bpy.app.timers.register() - Draw callbacks registered via
SpaceView3D.draw_handler_add() - Any background thread (threading.Thread, concurrent.futures)
Detection: Identify all function definitions. Check parent class. If parent inherits from bpy.types.Panel/Header/Menu/UIList/Gizmo, flag any bpy.ops call or data mutation inside draw().
2.2 Operator poll() Not Checked
Flag WARNING if code calls bpy.ops.* without verifying prerequisites:
bpy.ops.object.mode_set(mode='EDIT')without checkingcontext.active_object is not Nonebpy.ops.mesh.*without verifyingcontext.mode == 'EDIT_MESH'bpy.ops.object.modifier_apply()without checking object mode and modifier existence- Any
bpy.ops.view3d.*without verifying area type is'VIEW_3D'
2.3 Context Override Errors (Version-Critical)
Flag BLOCKER if code uses dict-style context overrides on Blender 4.0+:
# BLOCKER on 4.0+: Dict overrides removed
override = bpy.context.copy()
bpy.ops.something(override, ...)
Flag WARNING if code uses dict overrides without version gating (no bpy.app.version check).
Version matrix:
| Blender Version | Dict Override | temp_override |
|---|---|---|
| 3.0 – 3.1 | YES | NO |
| 3.2 – 3.6 | YES (deprecated) | YES |
| 4.0+ | REMOVED | YES |
CHECK 3: API Correctness
Severity: BLOCKER for removed APIs, WARNING for deprecated APIs
3.1 Removed API Usage
Scan for APIs removed in each version. Flag BLOCKER if used without version guard:
Removed in 4.0:
mesh.calc_normals()→ normals are auto-calculatedbone.layers→ usebone.collectionsarmature.bone_groups→ removedobj.face_maps→ use integer face attributesMeshEdge.bevel_weight→ usemesh.attributeswith float layerMeshEdge.crease→ usemesh.attributeswith float layerNodeTree.inputs.new()/NodeTree.outputs.new()→ useNodeTree.interface.new_socket()- Dict-style context overrides → use
context.temp_override()
Removed in 5.0:
bglmodule → usegpumoduledel obj["custom_prop"]for RNA properties → useobj.property_unset("prop")scene["cycles"]dict access → usescene.cyclesattribute accessimage.bindcode→ usegpu.texture.from_image()
Removed in 5.1:
Sequence.frame_final_start→ useSequence.frame_startSequence.frame_final_end→ useSequence.frame_end
3.2 Deprecated API Usage
Flag WARNING for deprecated APIs with known removal schedule:
bpy.utils.register_module(__name__)→ removed in 2.80, register classes individuallyobj.select = True→ useobj.select_set(True)(removed in 2.80)bpy.data.lamps→ usebpy.data.lights(removed in 2.80)mesh.uv_textures→ usemesh.uv_layers(removed in 2.80)bl_infodict in addon → useblender_manifest.tomlfor Blender 4.2+ extensions
3.3 Incorrect EEVEE Identifier
Flag BLOCKER if EEVEE identifier does not match target version:
| Version | Correct Identifier |
|---|---|
| 3.x | 'BLENDER_EEVEE' |
| 4.2–4.x | 'BLENDER_EEVEE_NEXT' |
| 5.0+ | 'BLENDER_EEVEE' |
CHECK 4: Data Safety
Severity: BLOCKER
4.1 Stale bpy.data References
Flag BLOCKER if code stores bpy.data.* references and uses them after:
- Any
bpy.ops.*call (operators can trigger undo) - Any
bpy.data.*.remove()call - Any
bpy.ops.wm.open_mainfile()/bpy.ops.wm.read_homefile() - Mode switches via
bpy.ops.object.mode_set() - Collection property
.add()/.remove()(re-allocates C arrays)
Detection: Track variable assignments from bpy.data.*. If the variable is used after any of the above operations without re-fetching → BLOCKER.
4.2 Name Collision on Data Creation
Flag WARNING if code does:
bpy.data.meshes.new("Name")
mesh = bpy.data.meshes["Name"] # May be "Name.001"
Correct pattern: mesh = bpy.data.meshes.new("Name") — capture the return value.
4.3 Missing Object-Collection Link
Flag BLOCKER if code creates a data block and object but never calls collection.objects.link(obj):
mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new("Object", mesh)
# MISSING: bpy.context.collection.objects.link(obj)
4.4 BMesh Lifecycle Errors
Flag BLOCKER if:
bm.verts[i]accessed without priorbm.verts.ensure_lookup_table()callbmesh.new()used without correspondingbm.free()(except edit-mode BMesh frombmesh.from_edit_mesh())bm.to_mesh(mesh)called withoutmesh.update()afterward
4.5 Missing mesh.update() After from_pydata()
Flag BLOCKER if mesh.from_pydata() is called without mesh.update() afterward.
CHECK 5: Threading Safety
Severity: BLOCKER
5.1 bpy Access from Background Threads
Flag BLOCKER if any bpy.* call occurs inside:
threading.Threadtarget functionsconcurrent.futures.ThreadPoolExecutor/ProcessPoolExecutorsubmitted callables- Any function passed to thread creation
Correct pattern: Use queue.Queue + bpy.app.timers.register() to relay results to main thread.
5.2 Unsafe Timer Callbacks
Flag WARNING if a timer callback performs heavy computation (loops over large data sets, file I/O). Timers run on the main thread and block the UI.
CHECK 6: Addon/Extension Structure
Severity: WARNING for structural issues, BLOCKER for registration errors
6.1 Registration Order
Flag BLOCKER if register() function:
- Registers operators/panels before their dependent PropertyGroup classes
- Creates PointerProperty referencing a type not yet registered
- Does not delete PointerProperty in
unregister()before unregistering the type
Correct order: Register dependencies first (PropertyGroup → Operator → Panel). Unregister in reverse order.
6.2 Handler Cleanup
Flag WARNING if addon registers handlers in register() but does not remove them in unregister().
6.3 Missing @persistent on Handlers
Flag BLOCKER if a handler callback is appended to bpy.app.handlers.* without the @bpy.app.handlers.persistent decorator. Without it, the handler is removed on file load.
6.4 Dynamic EnumProperty Items GC
Flag BLOCKER if EnumProperty(items=callback_function) where the callback returns a freshly created list without caching it in a persistent variable. The list gets garbage-collected → crash or empty dropdown.
6.5 Extension vs Legacy Addon Format
Flag INFO if target is Blender 4.2+ and code uses bl_info dict instead of blender_manifest.toml.
CHECK 7: Operator Best Practices
Severity: WARNING
7.1 Unnecessary bpy.ops Usage
Flag WARNING if bpy.ops.* is used where direct data access achieves the same result:
bpy.ops.object.location_clear()→obj.location = (0, 0, 0)bpy.ops.object.select_all(action='DESELECT')then individual select →obj.select_set()bpy.ops.transform.translate(value=...)→obj.location += Vector(...)
7.2 Missing Undo Support in Custom Operators
Flag WARNING if a custom Operator modifies data but does not set bl_options = {'REGISTER', 'UNDO'}.
CHECK 8: GPU / Drawing Code (Blender 5.0+)
Severity: BLOCKER
Flag BLOCKER if code imports bgl on Blender 5.0+. The bgl module is removed.
Flag WARNING if code uses bgl on Blender 4.x without version guard — bgl is deprecated since 3.x.
All drawing code MUST use the gpu module (gpu.shader, gpu.types.GPUBatch, gpu.state).
Severity Summary Decision Tree
Code will crash or corrupt data?
├── YES → BLOCKER
│ ├── Context access in restricted callback
│ ├── bpy access from thread
│ ├── Stale reference after undo/remove/mode-switch
│ ├── Removed API without version guard
│ ├── Missing ensure_lookup_table() on BMesh
│ ├── Missing bm.free() on standalone BMesh
│ ├── Missing collection.objects.link()
│ ├── Wrong registration order (PointerProperty before type)
│ ├── Missing @persistent on handler
│ ├── EnumProperty items GC (no cache)
│ ├── bgl import on Blender 5.0+
│ └── Missing mesh.update() after from_pydata()
├── NO → Code uses deprecated API?
│ ├── YES → WARNING
│ │ ├── Deprecated API with known removal version
│ │ ├── Dict context override on 3.2–3.6
│ │ ├── Operator without poll() pre-check
│ │ ├── Unnecessary bpy.ops usage
│ │ ├── Missing handler cleanup in unregister()
│ │ ├── Heavy computation in timer callback
│ │ └── Missing undo support in operator
│ └── NO → Best practice issue?
│ ├── YES → INFO
│ │ ├── No target version declared
│ │ ├── bl_info on 4.2+ (use blender_manifest.toml)
│ │ └── Style/naming conventions
│ └── NO → PASS
Auto-Fix Patterns
When a check fails, apply these fixes automatically where safe:
| Issue | Auto-Fix |
|---|---|
| Dict context override | Replace with context.temp_override() |
obj.select = True |
Replace with obj.select_set(True) |
bpy.data.lamps |
Replace with bpy.data.lights |
mesh.uv_textures |
Replace with mesh.uv_layers |
Missing mesh.update() after from_pydata() |
Insert mesh.update() after from_pydata() call |
Missing bm.free() |
Insert bm.free() after last bm.to_mesh() call |
Missing ensure_lookup_table() |
Insert bm.verts.ensure_lookup_table() before first index access |
Missing collection.objects.link() |
Insert bpy.context.collection.objects.link(obj) after objects.new() |
bgl import on 5.0+ |
Replace with gpu module equivalents |
scene["cycles"] on 5.0+ |
Replace with scene.cycles attribute access |
del obj["prop"] on 5.0+ |
Replace with obj.property_unset("prop") |
BLENDER_EEVEE on 4.2–4.x |
Replace with BLENDER_EEVEE_NEXT |
bone.layers on 4.0+ |
Replace with bone.collections usage |
NodeTree.inputs.new() on 4.0+ |
Replace with NodeTree.interface.new_socket() |
Missing @persistent on handler |
Add @bpy.app.handlers.persistent decorator |
Validation Report Format
After running all checks, produce a report:
## Validation Report
Target Version: Blender X.Y
Total Issues: N
### BLOCKERS (N)
- [CHECK X.Y] Description — file:line
### WARNINGS (N)
- [CHECK X.Y] Description — file:line
### INFO (N)
- [CHECK X.Y] Description — file:line
### Auto-Fixes Applied (N)
- Description — file:line
Reference Links
- Validation Rules: references/methods.md
- Before/After Examples: references/examples.md
- Anti-Patterns Catalog: references/anti-patterns.md
Dependency Skills
blender-core-api— bpy module structure and data access patternsblender-core-versions— Version matrix and breaking changesblender-core-runtime— Threading, handlers, timers, mathutilsblender-errors-context— Context error diagnosis and resolutionblender-errors-data— Data reference errors and safe access patternsblender-errors-version— Version compatibility error diagnosis
Official Documentation
- Blender Python API: https://docs.blender.org/api/current/
- Blender Gotchas: https://docs.blender.org/api/current/info_gotcha.html
- Blender 4.0 Release Notes: https://wiki.blender.org/wiki/Reference/Release_Notes/4.0
- Blender 5.0 Release Notes: https://wiki.blender.org/wiki/Reference/Release_Notes/5.0