flowchart TD
A[BlenderObject] --> B[AttributeArray]
B --> C[Attribute]
C --> D[bpy.types.Attribute]
A -.->|"Auto-sync, convenience methods"| E[User Level]
B -.->|"NumPy operations with auto-sync"| E
C -.->|"Manual control, one-shot operations"| F[Advanced Use]
D -.->|"Raw Blender API"| G[Low Level]
style A fill:#90EE90
style B fill:#87CEEB
style C fill:#FFB6C1
style D fill:#FFE4B5
Working with Attributes
The attribute system
Attributes and domains
In Blender, data on geometry is stored as attributes. An attribute is a named array with one value for each element of a domain, such as the vertices or faces of a mesh. Vertex positions, edge vertices, UV coordinates and vertex colors are all attributes.
For example: - Vertex positions are stored as a FLOAT_VECTOR attribute named "position" on the POINT domain - Face materials are stored as an INT attribute on the FACE domain - UV maps are stored as FLOAT2 attributes on the CORNER domain
databpy reads and writes these attributes as NumPy arrays.
The three-level API
databpy offers three levels of abstraction for working with attributes, each suited to different use cases:
1. BlenderObject: highest level
The BlenderObject class (nicknamed “bob”) wraps an object, with dictionary-style access to its attributes and properties such as bob.position.
import databpy as db
import numpy as np
# Create a simple object
bob = db.create_bob(np.random.rand(10, 3))
# Access attributes like a dictionary - returns AttributeArray
positions = bob["position"]
positions[:, 2] += 1.0 # Automatically syncs to Blender
# Or use the convenience property
bob.position[:, 2] += 1.0 # Same thing
# Store new attributes
bob["my_values"] = np.random.rand(10)When to use: Interactive work, scripting, when you want automatic syncing.
2. AttributeArray: NumPy arrays that sync
AttributeArray is a numpy.ndarray subclass holding a copy of an attribute’s values, which writes changes back to Blender as they are made.
import databpy as db
import bpy
obj = bpy.data.objects["Cube"]
# Create an AttributeArray
pos = db.AttributeArray(obj, "position")
# All NumPy operations work and auto-sync
pos[:, 2] += 1.0 # In-place addition
pos *= 2.0 # Broadcasting
pos[0] = [0, 0, 0] # Item assignment
# Changes are immediately reflected in BlenderCreating an AttributeArray (e.g. bob.position) reads the entire attribute into the array. Each modification then writes only the values it changed, directly into the attribute’s memory: pos[:10, 2] = 1.0 writes 10 values however large the mesh is. Values changed in Blender since the array was read are left alone.
STRING attributes are written in full instead, one value at a time, and an attribute that was removed is created again.
Indexing that returns a view (slices like pos[:, 2] or pos[2:5]) stays linked to Blender, so modifying the view syncs. Indexing that returns a copy (pos[mask], pos[[0, 3]], pos + 1, pos.copy()) gives a detached array that doesn’t sync:
pos = db.AttributeArray(obj, "position")
column = pos[:, 2]
column += 1.0 # a view - syncs
high = pos[pos[:, 2] > 1]
high += 1.0 # a copy - doesn't sync
pos[pos[:, 2] > 1] += 1.0 # assigning back through the original array syncsRead the array again after changing the geometry (e.g. adding vertices). Writing to an array whose attribute no longer matches raises a NamedAttributeError, and one whose object was removed raises a LinkedObjectError.
When to use: When you need NumPy-like operations with automatic syncing, working with position/color/custom data interactively.
3. Attribute: manual reads and writes
The Attribute class provides direct, stateless access with explicit control over when data is read or written.
import databpy as db
import numpy as np
import bpy
obj = bpy.data.objects["Cube"]
# Get the attribute wrapper
attr = db.Attribute(obj.data.attributes["position"])
# Manually read data
positions = attr.as_array()
# Perform operations (no auto-sync)
positions[:, 2] += 1.0
positions *= 2.0
# Manually write back (single write operation)
attr.from_array(positions)When to use: - One-shot read or write operations - Performance-critical code where you want control over sync timing - Batch processing where you make many whole-array changes before writing back - When you need to inspect attribute metadata (atype, domain, shape, storage_type) without reading data
Attribute types
Blender supports various attribute data types. databpy works with all of them through the AttributeTypes enum:
Float types
import databpy as db
import numpy as np
bob = db.create_bob(np.random.rand(10, 3))
# FLOAT - Single float per element
temperatures = np.random.rand(10).astype(np.float32)
bob["temperature"] = temperatures
# FLOAT2 - 2D vectors
uv_coords = np.random.rand(10, 2).astype(np.float32)
db.store_named_attribute(bob.object, uv_coords, "uv", atype="FLOAT2")
# FLOAT_VECTOR - 3D vectors (most common)
velocities = np.random.rand(10, 3).astype(np.float32)
bob["velocity"] = velocities
# FLOAT4 - generic 4D vectors (requires Blender 5.2)
if db.attribute.HAS_FLOAT4:
values = np.random.rand(10, 4).astype(np.float32)
db.store_named_attribute(bob.object, values, "values", atype="FLOAT4")
# FLOAT_COLOR - RGBA colors (4 components)
colors = np.random.rand(10, 4).astype(np.float32)
db.store_named_attribute(bob.object, colors, "color", atype="FLOAT_COLOR")
# QUATERNION - Rotations (4 components: w, x, y, z)
rotations = np.random.rand(10, 4).astype(np.float32)
db.store_named_attribute(bob.object, rotations, "rotation", atype="QUATERNION")
# FLOAT4X4 - 4x4 transformation matrices
matrices = np.random.rand(10, 4, 4).astype(np.float32)
db.store_named_attribute(bob.object, matrices, "transform", atype="FLOAT4X4")bpy.data.meshes['NewObject.001'].attributes["transform"]
Integer types
# INT - 32-bit signed integers
ids = np.arange(10, dtype=np.int32)
bob["id"] = ids
# INT8 - 8-bit signed integers (memory efficient)
small_values = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dtype=np.int8)
db.store_named_attribute(bob.object, small_values, "category", atype="INT8")
# INT16_2D - 2D 16-bit integer vectors
offsets = np.random.randint(-100, 100, size=(10, 2)).astype(np.int16)
db.store_named_attribute(bob.object, offsets, "offsets", atype="INT16_2D")
# INT32_2D - 2D integer vectors
pairs = np.random.randint(0, 100, size=(10, 2), dtype=np.int32)
db.store_named_attribute(bob.object, pairs, "edge_ids", atype="INT32_2D")bpy.data.meshes['NewObject.001'].attributes["edge_ids"]
Color types
# BYTE_COLOR - RGBA as unsigned 8-bit (0-255), read back as uint8
byte_colors = np.random.randint(0, 255, size=(10, 4), dtype=np.uint8)
db.store_named_attribute(bob.object, byte_colors, "vertex_color", atype="BYTE_COLOR")bpy.data.meshes['NewObject.001'].attributes["vertex_color"]
Boolean type
# BOOLEAN - True/False flags
selection = np.random.rand(10) > 0.5
bob["selected"] = selectionString type
# STRING - text values, read and written one value at a time
names = np.array([f"point_{i}" for i in range(10)])
bob["name"] = names/tmp/ipykernel_2632/2168651459.py:3: UserWarning: String attributes can be read and written through the Python API but are not yet properly supported within Geometry Nodes. Support may change in future Blender / databpy versions.
bob["name"] = names
STRING attributes can be read and written through the Python API but aren’t yet properly supported in Geometry Nodes, so databpy warns when they are used. They are also much slower than numeric attributes, as Blender can’t transfer them in bulk.
If you don’t specify an atype, databpy will infer it from your array’s dtype and shape using guess_atype_from_array():
| Array | Inferred type |
|---|---|
(n,) float / int / int8 / bool / str |
FLOAT / INT / INT8 / BOOLEAN / STRING |
(n, 2) float / int16 / other int |
FLOAT2 / INT16_2D / INT32_2D |
(n, 3) numeric |
FLOAT_VECTOR |
(n, 4) uint8 |
BYTE_COLOR |
(n, 4) other numeric |
FLOAT4 (FLOAT_COLOR before Blender 5.2) |
(n, 4, 4) numeric |
FLOAT4X4 |
FLOAT_COLOR and QUATERNION are never inferred, so pass them as atype.
Attribute domains
Attributes exist on different domains of the geometry, specified by the AttributeDomains enum:
| Domain | Description | Example Use Cases |
|---|---|---|
POINT |
Vertices, curve control points, point cloud points | Positions, vertex colors, temperature |
EDGE |
Mesh edges | Edge weights, crease values |
FACE |
Mesh faces/polygons | Material indices, face areas |
CORNER |
Face corners (vertex-face pairs) | UV coordinates, split normals |
CURVE |
Individual curve splines | Per-spline properties |
INSTANCE |
Geometry instances | Instance transforms, IDs |
LAYER |
Grease Pencil layers | Per-layer properties |
import databpy as db
import numpy as np
import bpy
obj = bpy.data.objects["Cube"]
# POINT domain (default) - one value per vertex
vertex_data = np.random.rand(len(obj.data.vertices), 3)
db.store_named_attribute(obj, vertex_data, "vertex_attr", domain="POINT")
# FACE domain - one value per face
face_data = np.random.rand(len(obj.data.polygons))
db.store_named_attribute(obj, face_data, "face_attr", domain="FACE")
# EDGE domain - one value per edge
edge_data = np.random.rand(len(obj.data.edges))
db.store_named_attribute(obj, edge_data, "edge_attr", domain="EDGE")bpy.data.meshes['Cube'].attributes["edge_attr"]
The length of your data array must match the number of elements in the target domain. A cube has 8 vertices, 12 edges, and 6 faces, so your arrays must have corresponding lengths.
Common workflows
Reading attributes
import databpy as db
import bpy
obj = bpy.data.objects["Cube"]
# Method 1: Simple function call (returns regular numpy array)
positions = db.named_attribute(obj, "position") # see named_attribute()
# Method 2: Via BlenderObject (returns AttributeArray)
bob = db.BlenderObject(obj) # see BlenderObject
positions = bob["position"] # or bob.position
# Method 3: List available attributes
attrs = db.list_attributes(obj) # see list_attributes()
print(attrs)
# Method 4: With modifier evaluation
evaluated_positions = db.named_attribute(obj, "position", evaluate=True)['.corner_edge', '.corner_vert', '.edge_verts', '.select_edge', '.select_poly', '.select_vert', '.uv_select_edge', '.uv_select_face', '.uv_select_vert', 'UVMap', 'edge_attr', 'face_attr', 'position', 'sharp_face', 'vertex_attr']
Writing attributes
import databpy as db
import numpy as np
bob = db.create_bob(np.random.rand(10, 3))
# Method 1: Dictionary-style (most convenient)
bob["my_data"] = np.random.rand(10, 3)
# Method 2: Function call (more options)
db.store_named_attribute(
bob.object,
data=np.random.rand(10),
name="custom_attr",
atype="FLOAT",
domain="POINT",
overwrite=True
)
# Method 3: Via BlenderObject method
bob.store_named_attribute(
np.random.rand(10, 3),
name="another_attr",
domain="POINT"
)bpy.data.meshes['NewObject.002'].attributes["another_attr"]
Modifying attributes in place
import databpy as db
import numpy as np
bob = db.create_bob(np.random.rand(100, 3))
# Using AttributeArray for interactive modifications
pos = bob.position
# Simple operations
pos[:, 2] += 1.0 # Move all points up
pos *= 2.0 # Scale positions
pos[pos < 0] = 0 # Clamp negative values
# Boolean indexing
selection = pos[:, 2] > 0.5
pos[selection, 2] = 1.0
# Column operations
pos[:, 0] = np.linspace(0, 1, len(pos)) # Linear ramp on XBatch processing
For large datasets, use the Attribute class to batch operations:
import databpy as db
import numpy as np
import bpy
obj = db.create_object(np.random.rand(int(1e5), 3))
attr = db.Attribute(obj.data.attributes["position"])
# Single read
positions = attr.as_array()
# Many operations without syncing
positions[:, 2] += 1.0
positions *= 2.0
positions = np.clip(positions, -10, 10)
# Single write
attr.from_array(positions)AttributeArray: 1 read, then each operation writes the values it changedAttribute: 1 read + 1 write for any number of operations (manual control)
Operations on a few values are cheap with AttributeArray, but each whole-array operation (pos *= 2.0) writes the whole array again. For many whole-array operations, Attribute is faster as it only writes once.
Integer attributes
import databpy as db
import numpy as np
bob = db.create_bob(np.random.rand(10, 3))
# Store integer IDs
ids = np.arange(10, dtype=np.int32)
bob["id"] = ids
# Retrieve as AttributeArray
id_array = bob["id"]
# Modify (automatically maintains int32 dtype)
id_array += 100
# Verify it's still integers
print(id_array.dtype) # int32int32
Boolean attributes
import databpy as db
import numpy as np
bob = db.create_bob(np.random.rand(20, 3))
# Create selection based on position
selection = bob.position[:, 2] > 0.5
bob["selected"] = selection
# Use boolean attribute for filtering
selected_mask = bob["selected"]
bob.position[selected_mask, 2] = 1.0Errors
databpy uses a consistent exception hierarchy for attribute-related errors:
Exception hierarchy
db.DatabpyError- base class for all databpy errorsdb.NamedAttributeError- raised when attribute operations fail- Used for: invalid names, domain size mismatches, type or domain conflicts with an existing attribute
db.AttributeNotFoundError(also aKeyError)- Raised when an attribute doesn’t exist, including
bob["missing"]
- Raised when an attribute doesn’t exist, including
db.AttributeMismatchError- Raised when data doesn’t match attribute expectations
- Used for: shape mismatches, values that don’t fit the attribute’s type
db.LinkedObjectError- raised when anAttributeArraycan’t sync because its object was removed or a file was loaded
NamedAttributeError currently also inherits from AttributeError, which will be removed in databpy 0.13.0 as hasattr() and getattr() silently swallow AttributeError. Catch NamedAttributeError or DatabpyError instead.
Common errors
import databpy as db
import numpy as np
import bpy
obj = bpy.data.objects["Cube"]
try:
# Trying to access non-existent attribute
data = db.named_attribute(obj, "nonexistent")
except db.AttributeNotFoundError as e:
print(f"Attribute not found: {e}")
try:
# Wrong data size for domain
db.store_named_attribute(obj, np.random.rand(100, 3), "test")
except db.NamedAttributeError as e:
print(f"Size mismatch: {e}")
try:
# Shape mismatch when using Attribute class
attr = db.Attribute(obj.data.attributes["position"])
wrong_shape = np.random.rand(8, 4) # Should be (8, 3)
attr.from_array(wrong_shape)
except db.AttributeMismatchError as e:
print(f"Shape error: {e}")Attribute not found: The selected attribute 'nonexistent' does not exist on the object. Available attributes: ['.corner_edge', '.corner_vert', '.edge_verts', '.select_edge', '.select_poly', '.select_vert', '.uv_select_edge', '.uv_select_face', '.uv_select_vert', 'UVMap', 'edge_attr', 'face_attr', 'position', 'sharp_face', 'vertex_attr']
Size mismatch: Data size 300 (shape (100, 3)) does not match the required size 24 for domain `POINT` with 8 elements and dimensions (3,)
Shape error: Array size 32 does not match attribute size 24. Array shape (8, 4) cannot be reshaped to attribute shape (8, 3)
Since AttributeNotFoundError and AttributeMismatchError inherit from NamedAttributeError, you can catch all attribute-related errors with a single except db.NamedAttributeError: clause, or every databpy error with except db.DatabpyError:.
Best practices
1. Choose the level of the API
- Interactive work, scripting: Use
BlenderObjectandAttributeArray - Performance-critical code: Use
Attributewith manual read/write - Quick one-off reads: Use
named_attribute()function
2. Match Blender’s data types
# Good: Explicit dtype matching Blender's storage
data = np.random.rand(10, 3).astype(np.float32)
# Less good: float64 will be converted to float32 anyway
data = np.random.rand(10, 3) # defaults to float643. Vectorize operations
# Not great: a Python loop with a sync per iteration
pos = bob.position
for i in range(len(pos)):
pos[i, 2] += 1.0 # writes a single value, but syncs every iteration
# Better: Vectorized operation (single sync)
pos[:, 2] += 1.0Each sync only writes the changed values, so occasional single-value writes are cheap, but the per-sync overhead adds up in a loop over every element.
4. Domains and types as enums or strings
Domains and types can be given as enums or as strings such as "POINT". Both are checked by type checkers, as the string parameters only accept the valid names.
from databpy import AttributeDomains, AttributeTypes
# equivalent to atype="FLOAT_VECTOR", domain="POINT"
db.store_named_attribute(
obj,
np.random.rand(len(obj.data.vertices), 3),
"my_attr",
atype=AttributeTypes.FLOAT_VECTOR,
domain=AttributeDomains.POINT
)
db.named_attribute(obj, "my_attr")array([[0.53907025, 0.29898933, 0.37800607],
[0.9290844 , 0.32685682, 0.8981513 ],
[0.56450593, 0.7567582 , 0.4331677 ],
[0.01150827, 0.7891584 , 0.9120895 ],
[0.03559079, 0.04118609, 0.6577564 ],
[0.40975833, 0.50005865, 0.1216043 ],
[0.5727442 , 0.08088566, 0.37693074],
[0.69431007, 0.10804583, 0.03235256]], dtype=float32)
5. Remove temporary attributes
# Remove attributes you no longer need
db.remove_named_attribute(obj, "my_attr")
try:
db.named_attribute(obj, "my_attr")
except db.NamedAttributeError as e:
print(e)The selected attribute 'my_attr' does not exist on the object. Available attributes: ['.corner_edge', '.corner_vert', '.edge_verts', '.select_edge', '.select_poly', '.select_vert', '.uv_select_edge', '.uv_select_face', '.uv_select_vert', 'UVMap', 'edge_attr', 'face_attr', 'position', 'sharp_face', 'test', 'vertex_attr']
See also
Classes
BlenderObject- High-level object wrapper with convenience methodsAttributeArray- Auto-syncing NumPy array subclassAttribute- Low-level attribute wrapper with manual control
Functions
named_attribute()- Read attribute data as NumPy arraystore_named_attribute()- Write attribute data to objectremove_named_attribute()- Delete an attributelist_attributes()- List all attributes on an objectcreate_bob()- Create a new BlenderObjectcreate_object()- Create a new Blender object
Enums
AttributeTypes- Enum of all available attribute data typesAttributeDomains- Enum of all available geometry domains
Exceptions
DatabpyError- Base exception for all databpy errorsNamedAttributeError- Base exception for attribute operationsAttributeNotFoundError- Exception for missing attributesAttributeMismatchError- Exception for data/type mismatches
External references
numpy.ndarray- NumPy array documentationbpy.types.Object- Blender Object documentationbpy.types.Attribute- Blender Attribute documentation