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 arbitrary key-value pairs stored on Blender's data-blocks (objects, meshes, scenes, etc.). Think of them as a built-in dictionary on each ID datablock, holding numbers, strings, arrays, or nested groups. They get used for:
- Rigging and animation: attaching custom parameters to bones or objects (e.g. a "stretch_length" on a bone that drives a constraint). These 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 stores settings for certain systems this way, Cycles being the notable example. Older Blender versions ignore unknown custom properties rather than failing to open the file.
The practical upshot: you can attach new data to a .blend without changing the DNA schema of the 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
ID properties support the following types:
- 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
lenfor 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
typecodemight be'i'(int),'f'(float),'d'(double), or'b'(boolean). Thesubtypefield of the IDProperty typically denotes the element type for arrays. For example, an IDProperty oftype=IDP_ARRAYwithsubtype=IDP_FLOATwould 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_IDcan 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 anImagedatablock 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
Groups can nest, so a custom property can itself contain a dictionary of sub-properties. Say we want to store an integer, a string, and a subgroup holding a float and an array on one object. The hierarchy looks 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
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.
This is where forward compatibility comes in. Because ID properties are unstructured extra data, a newer Blender can store new settings in an ID property instead of adding new DNA fields. An older Blender that doesn't know about the feature still opens the file; it finds an unknown ID property and ignores it. As long as it recognizes the IDProperty container structures, it won't crash. You may lose data if you then save from the older version, since it might not preserve that property when writing, but the file itself stays readable. Blender used this approach during the transition to Cycles, among other features.
From the RNA side, ID properties are reached two ways: high-level RNA functions for properties defined via bpy.props (more on that shortly), or the lower-level ID property API for truly dynamic props. The Python API provides an idprop.types module defining IDPropertyGroup and IDPropertyArray, so custom properties behave more like native Python objects. You get dictionary-like methods on an IDPropertyGroup (.keys(), .items(), .get()) and list-like methods on IDPropertyArray. Older Blender versions made this considerably more awkward.
The design in practice: render engine settings are the clearest example. When Blender 2.6x integrated Cycles, the developers used an ID property group rather than adding dozens of new fields to the Scene struct. scene.cycles is a dynamically-defined CyclesRenderSettings PropertyGroup, an IDProperty group under the hood, holding all Cycles-specific settings. Switch render engines and those properties stay stored but unused. Open the file in a Blender without Cycles and the scene carries an unknown "cycles" custom property that gets ignored rather than dropped. Engine-specific data stays out of the core file format, and a new engine 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 live in ID.properties. Both scene.cycles and scene.eevee are custom property groups on the Scene, which is why render engines can be developed independently of the core file format, and why a .blend saved with Cycles opens in a build without it.
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. To retrieve the root property group of an ID and get a specific custom property:
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 gets the object's ID properties, 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 utility functions alongside it: IDP_AddToGroup(idprop_group, prop) adds a new property to a group, IDP_New(type, template, name) creates a new IDProperty of a given type using a template for the initial value. Creating a 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);IDPropertyTemplate is a union used to pass initial values: we set array.len and array.type for the array property, but the group needs nothing (hence val left uninitialized there). Blender's ID property API handles memory allocation and linking. It's low-level C, so you manage types yourself.
Python API example: any ID-type object behaves like a dict for custom properties:
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"]) # 10When 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 the values are still stored as ID properties, but Blender registers it as an RNA property, so it gets a dedicated UI entry instead of appearing in the generic Custom Properties panel. The difference is timing: these are defined upfront, so Blender knows their type and limits from the class, whereas square-bracket custom properties are dynamic at runtime. Both end up saved as IDProperty data. Use bpy.props when you want a properly integrated property with a UI name and tooltip, and you'll access it like any attribute (mat.my_float = 1.25). Use direct assignment for one-off storage or nested data that isn't practical to declare as a formal RNA property.
Use case: Cycles render settings via ID properties
Blender's Cycles engine settings are the clearest example of this in production code. Rather than bloating the Scene DNA with dozens of new fields for samples, bounces and the rest, the developers put everything in a PropertyGroup attached to Scene as an ID property. When you switch the render engine to Cycles, the panels you see (Samples, Light Paths) read from bpy.context.scene.cycles.*, and every one of those sub-properties is dynamically defined. The Cycles add-on registers them with PointerProperty as shown above; each setting like samples is an IntProperty in the CyclesRenderSettings group, declared in Python.
Why this design? Third-party render engines can define their own settings the same way without touching Blender's core structs. And a .blend that used Cycles still opens in a build without Cycles, carrying an unknown Scene.cycles custom property. You lose those settings if you save again from that build, but the file itself stays accessible. EEVEE later used the same pattern with scene.eevee, as can any add-on-defined engine.
Best practices and tips
For core and add-on developers:
-
Use ID properties for extendable settings. If your feature stores data per data-block, and especially if older Blenders or optional add-ons might open the file, ID properties beat new DNA fields. Geometry Nodes uses them 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 reads or modifies custom properties frequently:
IDP_GetProperties,IDP_GetPropertyFromGroup,IDP_New. Checkprop->typebefore casting data. If you allocate large arrays, free the IDProperties properly when you remove them, though Blender's higher-level API usually handles this on save and free. -
IDProperty and RNA. When you register properties via
bpy.props, Blender may use IDProperties to store the values, but you don't interact with that level directly. One caveat: pointer properties cannot point to sub-data like a bone or a modifier. The target must be a full ID block (Object, Mesh, Image) or a PropertyGroup. -
Storing references. The
IDP_IDtype stores a datablock reference. Storing the image's name as a string breaks when the image is renamed; an ID property of type ID keeps the link intact, the same way normal ID user pointers behave. Create them viaPointerPropertyin Python or the C API.
For technical artists and scripters:
- Adding custom properties.
obj["my_prop"] = 10in Python, or the Custom Properties panel -> Add in the UI. Both do the same thing. The UI's Edit dialog sets limits, defaults, and tooltips. These properties can drive modifiers, constraints, and shaders, and the Attribute node in materials reads object custom properties by name, so you can vary shader values per object without duplicating the material. - Animation and drivers. Custom properties are fully animatable. Insert keyframes and Blender creates an F-Curve under the object's animation data. You can also use them in driver expressions: a custom float "wing_angle" on a bird rig can drive several bones' rotations, referenced as
var = object["wing_angle"]. - Using PropertyGroups for organization. With many related properties, especially in an add-on, define a
PropertyGroup(a Python class subclassingbpy.types.PropertyGroupwith fields frombpy.props) and attach it as a single pointer property. You getobj.my_toolSettings.scale_factorinstead of a flat pile ofobj["tool_scale"],obj["tool_enable"], and the UI groups them under one expandable section. It's still an IDProperty group underneath, just managed through RNA. - Limitations. ID properties don't store arbitrary Python objects, only the basic types above. Assign a list of mixed types or a complex object and Blender won't know how to serialize it; stick to numbers, strings, dicts and lists, with simple or further-nested contents. Large data (thousands of array elements) bloats the .blend and isn't memory-efficient here, so use external files or a proper data-block type for big datasets. ID properties suit small-to-moderate data that needs to travel with the file. Lookup itself is fast, essentially a pointer lookup, though searching by name across a very large number of properties adds overhead. In practice that rarely bites.
Tip: remove a property with del obj["propname"], and test for one with if "propname" in obj:, just like a dict. obj.keys() lists custom property names. Note that obj.items() includes built-in RNA properties alongside custom ones, so use the id_properties API or the bracket interface when you want only custom props.
Conclusion
ID properties let you extend Blender's data model without touching the DNA schema, which is why they show up everywhere from rigs to render engines. If you're storing pipeline metadata, exposing rig sliders, or writing settings for an engine that doesn't exist yet, this is the mechanism to reach for.
The main things to remember: they're a dict on every data-block, they survive round-trips through Blender versions that don't understand them, and they're the wrong place to put a large dataset.
