Introduction
In Blender's architecture, ID properties (often called custom properties) are a mechanism to attach arbitrary user-defined data to any data-block (ID) without modifying core data structures. They were introduced to allow users, add-ons, and even Blender itself to store extra information (for rigging, scripts, engine settings, etc.) in .blend
files in a forward-compatible way. This article follows up on a previous discussion of Blender's RNA/DNA system by focusing exclusively on ID properties - how they work in memory, how they're saved, their data types, code examples, and practical uses for developers and technical artists.
What Are ID Properties and Why Use Them?
ID properties are essentially arbitrary key-value pairs stored on Blender's data-blocks (objects, meshes, scenes, etc.). They serve as a flexible "extension slot" for data: you can think of them as a built-in dictionary on each ID datablock. The core purpose is to enable storing custom data (numbers, strings, arrays, etc.) directly on an ID. This is useful for:
- Rigging and Animation: attaching custom parameters to bones or objects (e.g. a "stretch_length" on a bone that drives a constraint). Such properties can be animated or driven just like built-in properties.
- Add-ons and Pipeline: scripts can store configuration or metadata (e.g. an asset ID, or a boolean to tag objects for export) without needing external files.
- Engine and Feature Settings: Blender itself uses ID properties to store settings for certain systems (one notable example is Cycles render engine settings, discussed later). This design allows new features to save data in the .blend that older versions won't choke on - older Blender versions simply ignore unknown custom properties, rather than failing to open the file.
In summary, ID properties are Blender's built-in solution for forward-compatible data extension: they let you attach new data to files without changing the DNA schema of Blender's file format.
Internal Structure and Storage in .blend Files
Internally, an ID property is represented by a C struct IDProperty
defined in Blender's source. Every data-block's base struct (ID
) includes a pointer to a linked list of IDProperty structs. For example, the C definition of ID
has an IDProperty *properties;
member. If an ID has any custom properties, this pointer references a root IDProperty of type "Group" (dictionary) that contains all the custom properties as children.
Key fields in the IDProperty
struct include: a name (the property's key), a type and subtype code, a union for the value data, and length fields. Below is a simplified version of the struct definition from Blender's DNA (comments omitted for brevity):
typedef struct IDPropertyData {
void *pointer; /* For pointer/array types */
ListBase group; /* For group/dictionary type */
int val; /* For int values (or other inline data) */
int pad; /* Padding */
} IDPropertyData;
typedef struct IDProperty {
struct IDProperty *next, *prev;
char name[MAX_IDPROP_NAME]; /* e.g. "my_prop" */
char type, subtype;
short flag;
IDPropertyData data;
int len; /* For strings/arrays: length (or string length + 1) */
int totallen; /* Allocated array/string buffer length */
int saved; /* Runtime flag for file save, not preserved in file */
} IDProperty;
When you add a custom property to an ID, Blender will ensure the ID's properties
pointer is initialized to a Group IDProperty (think of it as the root dictionary). All user-defined properties on that ID become elements in this group. If the group doesn't exist yet, calling the utility function IDP_GetProperties(id, create_if_needed=true)
will create an empty Group property and attach it to the ID. This root group itself isn't exposed to users; it's an implementation detail. In the .blend file, the entire tree of IDProperty structs (the group and its children) is saved as part of the ID datablock. Because the .blend format serializes Blender's DNA structs, these custom properties are saved transparently alongside built-in data. An older Blender can open the file and skip over unrecognized ID properties without issue (they'll be ignored unless specifically preserved), which is why this system aids forward compatibility.
Memory layout: Each IDProperty can either store data directly (for simple types) or point to allocated memory (for arrays, strings, etc.). For example, an integer or float is stored in an int val
(or in a similar field, possibly using the union), whereas a string is stored as a char array allocated and referenced by data.pointer
, and a group uses the data.group
list to link child properties. The len
and totallen
fields track the length of arrays or strings and the total allocated size. This design was influenced by how Python lists allocate buffers (to efficiently append to arrays, for instance).
Supported Data Types in ID Properties
Blender's ID properties support a variety of data types, covering most common needs:
- Integers: Stored as 32-bit integers. (There is also a flag for treating an int as Boolean in UI, but at the DNA level it's an int type.)
- Floats: Floating-point values. Internally, Blender often uses double precision (64-bit) to store these for accuracy, although they may be presented as "float" in UI.
- Strings: Text values (char arrays). These are stored with a null terminator; the
len
for a string property equals the string length + 1. - Arrays: Homogeneous arrays of simple types. You can have an array of ints, floats, doubles, or booleans. In the API, an IDProperty array's
typecode
might be'i'
(int),'f'
(float),'d'
(double), or'b'
(boolean). Thesubtype
field of the IDProperty typically denotes the element type for arrays. For example, an IDProperty oftype=IDP_ARRAY
withsubtype=IDP_FLOAT
would be an array of floats. - Groups (Dictionaries): A group (
type=IDP_GROUP
) acts like a dictionary or struct, containing a list of child IDProperty elements. Each child has its own name and type. Groups can be nested inside other groups, allowing hierarchical data. - ID (Datablock Pointer): Special type for referencing another Blender ID (added around Blender 2.78/2.79). An ID property of type
IDP_ID
can store a pointer to another data-block (e.g., an Object, Image, etc.). This was introduced to allow custom properties that reference Blender datablocks (similar to pointers in RNA). For example, an add-on could store a reference to anImage
datablock as a custom property on a Material. (When saved, Blender will preserve the link by name and restore it, much like how regular ID links are handled.)
Blender's source defines these type codes in an enum (or defines): e.g. IDP_INT (1)
, IDP_FLOAT (2)
, IDP_ARRAY (5)
, IDP_GROUP (6)
, IDP_ID (7)
, etc.. There are also historical types like IDP_VECTOR
and IDP_MATRIX
(for 3-element or 4x4 float arrays) listed, which are essentially specialized array forms. In modern usage, vectors and matrices are typically just stored as numeric arrays with appropriate lengths (3 or 16), possibly with RNA subtype hints for UI.
Nested Structure Example
ID properties can be nested to form complex data structures. For instance, you might have a custom property that itself contains a dictionary of sub-properties. Consider an example where we want to store some extra data on an object: an integer, a string, and a subgroup of miscellaneous data including a float and an array. The hierarchy would look like this:
Object (ID datablock)
└── properties (Group)
├── my_int = 42 (Int)
├── my_string = "Hello World" (String)
└── my_group (Group)
├── nested_float = 3.14 (Float)
└── nested_array = [1, 2, 3] (Array of Int)
Each indent level represents children of a Group property. In memory and the .blend file, this would be represented by a tree of IDProperty
structs linked via their data.group
lists. In the Blender UI, you would typically see my_int
, my_string
, and my_group
(expandable to show its members) under the Object's Custom Properties panel.
How ID Properties Integrate with RNA (and Support Forward Compatibility)
Blender's RNA system (its introspection and UI schema for properties) treats ID properties in a special way. All ID properties on an ID datablock are exposed through a generic API and UI panel (the Custom Properties panel). When you add a custom property, Blender's RNA registers it dynamically so that it can be accessed like a built-in property in Python and drivers. For example, if you add an ID property "foo"
on object obj
, you can access it in Python as obj["foo"]
, and you can even animate it or driver-link it (it will have an RNA path like object["foo"]
). The Blender UI will list it under Custom Properties, and you can edit its value and some metadata (like min/max, default, tooltip) via a GUI dialog. Internally, any UI metadata you set (using the Edit Custom Property dialog) is stored as sub-properties in a special group attached to the property. For instance, Blender might create sub-IDs for min, max, description, etc., in a hidden group alongside the value. This allows Blender to remember the UI settings of the property.
One of the major advantages of ID properties is forward compatibility. Because ID properties are essentially unstructured extra data, a newer version of Blender can store new kinds of settings in an ID property instead of adding new DNA fields. An older Blender version (that doesn't know about this new feature) will still open the file - it will find an unknown ID property and typically just ignore it (it won't know how to use it, but the data is still present). As long as the older Blender at least recognizes the IDProperty container structures, it won't crash. There may be some loss of data if you save from the older version (since it might not preserve that property when writing, or the data becomes orphaned), but critical data can be marked as ID properties to avoid breaking the file entirely. This approach was used, for example, during the transition to the Cycles render engine and other features.
From the RNA perspective, ID properties are accessed via the API in two ways: (1) High-level RNA functions (for those defined via bpy.props
- more on that shortly), or (2) the lower-level ID property API for truly dynamic props. The Blender Python API now provides the idprop.types
module which defines IDPropertyGroup
and IDPropertyArray
classes, making custom properties act more like native Python objects. You can call dictionary-like methods on an IDPropertyGroup (like .keys(), .items(), .get()
) and list-like methods on IDPropertyArray. This is a big improvement from older Blender versions, making custom properties easier to work with in Python.
Forward-Compatible Design in Action: A great example inside Blender is the render engine settings. When Blender 2.6x first integrated Cycles (which was new at the time), instead of adding dozens of new fields to the Scene struct for Cycles settings, the developers used an ID property group. The Scene has a pointer property called scene.cycles
which is a dynamically-defined CyclesRenderSettings
PropertyGroup - essentially an IDProperty group under the hood - containing all Cycles-specific settings. If you switch to a different render engine, those Cycles properties remain stored but are unused. If you open the file in an older Blender (which doesn't know about Cycles), the scene still has an unknown "cycles" custom property (Group) attached; older Blender will ignore it, but the data isn't lost. This design decouples engine-specific data from the core file format. It's also extensible: new render engines (like EEVEE) can do the same without clashing. In Python API terms, the Cycles add-on registers something like:
bpy.types.Scene.cycles = PointerProperty(
name="Cycles Render Settings",
type=CyclesRenderSettings, # a PropertyGroup class
description="Cycles render settings"
)
In C, these engine settings ultimately reside in ID.properties
. The success of this approach is evident - you can see scene.cycles
and scene.eevee
in Blender's API, both are custom property groups on the Scene. The choice to use ID properties here was made to allow modular development of render engines and to ensure that even if a .blend is opened in a build that doesn't have Cycles, it wouldn't outright fail. It's a forward-compatible strategy that keeps unknown data in the file.
Code Snippets: Using ID Properties in C and Python
C API Example: Blender's kernel offers a set of functions in BKE_idprop.h
for working with ID properties at the C level. For instance, to retrieve the root property group of an ID and get a specific custom property, one can do:
IDProperty *id_props = IDP_GetProperties(&object->id, false);
IDProperty *prop = IDP_GetPropertyFromGroup(id_props, "custom_data");
if (prop && prop->type == IDP_ARRAY && prop->subtype == IDP_FLOAT) {
float *values = (float *)prop->data.pointer;
/* use the values ... */
}
This snippet (adapted from a suggestion by a Blender developer) gets the object's ID properties, then finds the property named "custom_data"
in the group, and if it's an array of floats, obtains the raw float pointer. The API provides many utility functions: e.g., IDP_AddToGroup(idprop_group, prop)
to add a new property to a group, IDP_New(type, template, name)
to create a new IDProperty of a given type using a template for initial value, etc. For example, creating a new group and a float array property in C:
IDPropertyTemplate val;
IDProperty *root = IDP_GetProperties(id, true); // get root group, create if needed
IDProperty *group = IDP_New(IDP_GROUP, val, "group1"); // new empty group property
val.array.len = 4; val.array.type = IDP_FLOAT;
IDProperty *color = IDP_New(IDP_ARRAY, val, "color1"); // new float array of length 4
IDP_AddToGroup(group, color);
IDP_AddToGroup(root, group);
In the above, IDPropertyTemplate
is a union used to pass initial values: we set array.len
and array.type
for the array property, but for the group we don't need to set anything (hence val
is left uninitialized for the group creation). Blender's ID property API handles memory allocation and linking. It's low-level C code, so you have to manage types carefully, but it gives full control.
Python API Example: In Python, using ID properties is straightforward. Any ID-type object behaves like a dict for custom properties. You can do:
obj = bpy.context.object
# Assign various custom properties:
obj["stage"] = "demo" # string property
obj["version"] = 3 # int property
obj["thresholds"] = [0.1, 0.5, 1.0] # array (list) of floats
obj["options"] = {"enabled": True, "max": 10} # nested group (dict)
# Reading them:
print(obj["stage"], obj["version"]) # 'demo', 3
print(obj["thresholds"][1]) # 0.5
print(obj["options"]["max"]) # 10
When you assign using obj[...] = ...
, Blender automatically creates or updates the corresponding IDProperty. Basic Python types (int
, float
, str
, bool
) and lists/dicts composed of those are converted to IDProperty types appropriately. Note that Python lists become IDProperty arrays if all elements are numbers or booleans. The printed type(obj["thresholds"])
might appear as a normal list in some cases, but under the hood it's a special IDPropertyArray
- you can call obj["thresholds"].to_list()
to get a regular Python list copy if needed. Similarly, obj["options"]
behaves like a dict but is actually an IDPropertyGroup
object; you can iterate over it or call obj["options"].keys()
etc..
For UI and animation, Blender treats these custom properties as part of the object's RNA: you can right-click a custom property and add a driver or keyframe, and access it in expressions (e.g., obj["stage"]
in a driver). The Blender UI also provides a way to set limits, default, and description on custom props (as mentioned, Blender stores those as hidden sub-properties). Keep in mind that if you remove a custom property, any drivers or animations on it will break, so manage their lifecycle accordingly.
Another way to define properties in Python is via the bpy.props
module for use in add-ons or custom classes. For example, you can define a new property on an existing Blender type:
import bpy
bpy.types.Material.my_float = bpy.props.FloatProperty(name="My Float", default=0.0)
This attaches a new Float property to Material datablocks. Under the hood, this is implemented using ID properties as well (for storing the values), but Blender registers it as an RNA property (so it shows up in the UI not in the generic Custom Properties panel, but as a dedicated UI entry). The distinction is that these properties are defined upfront (so Blender knows their type/limits from the class), whereas the square-bracket custom properties are truly dynamic at runtime. Both end up saved as IDProperty data. The bpy.props approach is great for add-on developers who want nicely integrated properties (with proper UI name, tooltip, etc.) - once registered, you use them like any attribute (e.g., mat.my_float = 1.25
). The direct assignment (IDProperty) approach is handy for quick one-off storage or when you need to store complex nested data that might not be practical to define as formal RNA properties.
Use Case: Cycles Render Settings via ID Properties
As mentioned earlier, Blender's Cycles engine settings are a prime example of ID properties in action. When Cycles was added, rather than bloating the Scene DNA with dozens of new fields (like samples, bounces, etc.), developers opted to use a PropertyGroup to contain all Cycles settings. This CyclesRenderSettings
property group is attached to Scene
as an ID property. In practice, when you toggle the render engine to "Cycles" in Blender, the UI panels you see (Samples, Light Paths, etc.) are reading from bpy.context.scene.cycles.*
- all those sub-properties (e.g., scene.cycles.samples
, scene.cycles.max_bounces
) are dynamically defined. They are implemented by the Cycles add-on (or integrated module) using PointerProperty
as shown earlier. Each setting like samples
is a IntProperty
in the CyclesRenderSettings
group, defined in Python in the Cycles module.
Why this design? Because it allows Blender to remain flexible and extensible: third-party render engines can define their own settings in a similar way without needing to alter Blender's core structs. It also means that if you open a .blend that used Cycles in a version of Blender that doesn't have Cycles, the file still opens - it simply has an unknown Scene.cycles
custom property. There's some data loss (that Blender won't use those settings and might drop them if you save again), but importantly, the file is accessible. If you later open that file in a Cycles-enabled build, your settings can potentially be recovered if they were preserved. In practice, Blender tries to keep such custom data around unless explicitly removed. This use of ID properties for render engine settings was forward-thinking: it paved the way for integrating EEVEE (which similarly uses scene.eevee
for its settings) and even user-defined render engines via add-ons can store settings without conflict. It exemplifies how ID properties support modularity and future expansion.
Best Practices and Tips for Using ID Properties
For Core/Addon Developers:
-
Use ID Properties for Extendable Settings: If you're adding a feature that needs to store data per data-block (and especially if older Blenders or optional add-ons might open the file), consider using ID properties instead of adding new DNA fields. This is common for experimental features or add-on data. For example, Geometry Nodes in recent versions use ID properties to store arbitrary per-object data and node tree data in the
.blend
. -
Access via API: Use the BKE_idprop API in C for performance-critical code that needs to retrieve or modify custom properties frequently. The functions like
IDP_GetProperties
,IDP_GetPropertyFromGroup
,IDP_New
, etc., are your tools. Remember to handle types correctly - checkprop->type
before casting data. Also, be mindful of memory: if you allocate large arrays, make sure to free IDProperties properly if you remove them (Blender's higher-level API usually handles this on file save and free). -
IDProperty and RNA: When you register properties via
bpy.props
in Python (for UI integration), Blender behind the scenes may use IDProperties to store the values, but you don't interact with that level directly. One thing to note: embedded IDs vs. regular IDs - pointer properties cannot point to sub-data like a bone or a modifier (only to full ID blocks like Object, Mesh, Image, etc.). Keep that in mind if you attempt to make a PointerProperty to something; it must be a proper ID or PropertyGroup. -
Storing References: With
IDP_ID
type, you can store a datablock reference. For instance, you might store a custom property on an object that references a specific Image datablock (perhaps for a custom texture assignment). Rather than storing just the image's name (string), using an ID property of type ID ensures that Blender will maintain the link (and update it if the image is relinked or renamed, similar to how it handles normal ID user pointers). Create such properties viaPointerProperty
in Python or the C API as needed. This is much safer than storing names/paths that can break.
For Technical Artists / Scripters:
- Quickly adding Custom Properties: The simplest way to add a custom property is through Python: e.g.
obj["my_prop"] = 10
or via the UI (Custom Properties panel -> Add). Both achieve the same result. Use the UI's Edit dialog to set limits, defaults, and tooltips for nicer behavior. These properties can drive modifiers, constraints, shaders, etc. (The Attribute node in materials can even read object custom properties by name, enabling you to tweak shader values per object without new material instances!). - Animation and Drivers: Custom properties are fully animatable. You can insert keyframes on them and Blender will create an F-Curve under the object's animation data. You can also use them in driver expressions. For example, you might have a custom float property "wing_angle" on a bird rig, and drive multiple bones' rotations from it. To reference it in a driver, use the path like
var = object["wing_angle"]
. (Blender will internally map this to the IDProperty and update accordingly.) - Using PropertyGroups for organization: If you have many related custom properties, especially in an add-on, consider creating a
PropertyGroup
(via Python class withbpy.types.PropertyGroup
and multiple fields defined withbpy.props
). You can then attach it as a single pointer property to the ID. This gives a cleaner structure in Python (e.g.obj.my_toolSettings.scale_factor
instead ofobj["tool_scale"]
andobj["tool_enable"]
, etc.). The UI will also display them under a common panel/expandable section rather than a long flat list. Internally, this is still using an IDProperty group, but it's managed through RNA. - Limitations: Be aware of a few limitations of ID properties. First, they do not support storing arbitrary Python objects - only the basic data types discussed. If you assign a list of mixed types or a complex object, Blender won't know how to serialize that. Stick to numbers, strings, dicts, lists (and in lists/dicts, the contents must also be simple types or further nested dicts/lists). Second, large data (like thousands of elements in an array) can bloat the .blend and may not be memory efficient to keep as an ID property. If you need to store big datasets, consider alternate solutions (like external files or new data-block types). ID properties are best for small-to-moderate amounts of data that need to travel with the Blender file. Lastly, performance: accessing an ID property is very fast (essentially a pointer lookup in a linked list or hash in newer implementations), but if you have a huge number of them, there could be overhead in searching by name. In practice this is rarely an issue, just something to keep in mind (Blender has been improving this system over time).
Tip: If you need to remove an ID property via Python, use del obj["propname"]
. And if you want to test for existence, do if "propname" in obj:
just like a dict. You can also use obj.keys()
to list custom property names. When iterating over properties, note that obj.items()
will include custom props as well, but also some built-in RNA properties - to get only custom ID properties, use the id_properties
API or simply stick to the bracket/dict interface which is intended for them.
Conclusion
Blender's ID property system is a powerful feature enabling customization and extension of Blender's data model without breaking compatibility. By understanding the underlying structure (DNA storage and the runtime RNA integration), developers can use ID properties to store custom data reliably, and technical artists can harness them to drive creative rigs and tools. Whether it's an addon storing pipeline metadata, a rig builder exposing tweakable sliders, or Blender's own Cycles engine saving render settings, ID properties provide the needed flexibility. They are essentially the "extra pockets" of every Blender data-block - use them wisely to keep your data organized, and Blender will carry that data wherever your .blend goes!