Using nodebpy
Using nodebpy inside of your own add-ons & projects.
The package is available on PyPI here. You can install it and use it in your own projects using pip or your preferred package manager, I recommend uv.
pip install nodebpy
uv add nodebpyThis will allow it to be shipped with add-ons, following their guidelines on how to bundle .whl files or vendoring.
nodebpy should be forward-compatible, but likely backwards incompatible with older versions of Blender, as new nodes are added the code will break if trying to use them in older versions of Blender.
Versioning nodebpy
For clarity around which versions of Blender nodebpy supports, it will be versioned with a modified semantic versioning scheme.
The scheme will be: BLENDER_VERSION.MINOR_VERSION.PATCH_VERSION. For example Blender 5.2.0 would be 520.0.0. Major version changes that are compatible with Blender 5.2.0 would be 520.X.Y, where extra features could be added in 520.1.0 and a bug fix or patch could be added in 520.0.1. Version 520.X.Y should be forward compatible with Blender 5.3+, but incompatible with Blender 5.1 and earlier.
Development and updates will mostly happen on the later versions as Blender adds new updates and features, but some fixes and patches may be backported to older versions as needed.
Re-running code in a live session
An editor that re-runs a script on every change — a Text block, a file watched on disk — needs more than exec. nodebpy.live.run_source executes the source and takes care of the rough edges of running it again:
from nodebpy.live import run_source
result = run_source(text.as_string(), filename=text.name)
result.tree # the tree the run produced (or None)
result.created # every node group the run added- Node groups the code’s
Custom*Groupclasses claim by_nameare moved out of the way first, socreate_group()rebuilds them from the current code instead of handing back the previous build. Afterwards the old trees’ users (group nodes, modifiers, pinned editors) are remapped onto the new builds and the old trees removed; if the run fails, the old names are restored and the original exception is re-raised. - Input values on Geometry Nodes modifiers are kept across a rebuild of the tree’s interface, matched by socket name (a rebuilt interface gets new socket identifiers, which is what the modifier keys its values on). Inputs that read a named attribute stay that way. Values are reapplied even when the run fails, so a half-built
clear=Truetree keeps the values of the sockets it did build. - Tracebacks carry
filenameand the real line numbers. - Groups linked from an asset library cannot be renamed, so a class whose
_nameis an already-appended asset reuses it rather than rebuilding it.
Pair it with to_python(in_place=True), whose header rebuilds the exported tree in the same datablock (see Nodes to Code).
Using multiple versions
If you use nodebpy in your add-on you likely support multiple versions of Blender.
Each version of nodebpy will target a specific Blender release, but you can import multiple versions if you vendor the package inside of your add-on.
YourAddon/
├── lib/
| ├── __init__.py (empty)
│ ├── nodebpy/
│ │ ├── __init__.py
│ │ ├── _520/
│ │ │ ├── src/nodebpy (5.2.0)
│ │ ├── _530/
│ │ │ ├── src/nodebpy/ (5.3.0)
└── ...
lib/nodebpy/__init__.py
import bpy
_v = bpy.app.version
if _v == (5, 2, 0):
from ._520.src.nodebpy import *
elif _v >= (5, 3, 0):
from ._530.src.nodebpy import *
else:
raise ImportError(f"Unsupported Blender version: {_v}")This way you can import the correct version of nodebpy based on the Blender version at runtime.
It’s up to your own usual versioning code to ensure that code used is compatible with the right Blender versions, but you should be able to just import from .lib.nodebpy like regularly using the package.
from bpy.types import GeometryNodeTree
from .lib.nodebpy import geometry as g
def default_tree(count: int = 10) -> GeometryNodeTree:
with g.tree("Default Tree Name") as tree:
(
tree.inputs.integer("Count", count)
>> g.Points(position=g.RandomValue.vector())
>> g.InstanceOnPoints(instance=g.IcoSphere())
>> tree.outputs.geometry("Result")
)
return tree.tree
default_tree()