TMHSDigital

custom-properties

Define and bind Blender custom properties via bpy.props using the type annotation form, with PropertyGroup for grouping, PointerProperty for binding, and the four storage location options for Scene/Object/WindowManager/AddonPreferences. Targets 5.2 LTS.

TMHSDigital 17 3 Updated 1mo ago
GitHub

Install

npx skillscat add tmhsdigital/blender-developer-tools/custom-properties

Install via the SkillsCat registry.

SKILL.md

Custom Properties

Trigger

Use this skill when the user:

  • Wants to attach data to objects, scenes, or other Blender datablocks that survives save and load
  • Mentions bpy.props, PropertyGroup, PointerProperty, "custom properties"
  • Is unsure where to store add-on settings
  • Sees a deprecation warning about property assignment

Required inputs

  • What you want to store: a single number, a small struct, a list of items, or a complex hierarchy
  • Where it should live: Scene (per-document), Object (per-object), WindowManager (session-only), AddonPreferences (per-user)
  • Whether it must persist through save/load (almost always yes)

The type annotation form (mandatory since 2.8)

import bpy


class MY_ADDON_PG_settings(bpy.types.PropertyGroup):
    intensity: bpy.props.FloatProperty(
        name="Intensity",
        description="How strong the effect is",
        default=1.0,
        min=0.0,
        max=10.0,
    )
    use_smoothing: bpy.props.BoolProperty(name="Smooth", default=True)
    mode: bpy.props.EnumProperty(
        name="Mode",
        items=[
            ('FAST', "Fast", "Lower quality, faster"),
            ('GOOD', "Good", "Balanced"),
            ('BEST', "Best", "Best quality, slower"),
        ],
        default='GOOD',
    )

The properties are declared with : (PEP 526 annotations), not = (assignment). Blender's metaclass scans the annotation namespace and registers each bpy.props.* it finds.

The assignment form (intensity = bpy.props.FloatProperty(...)) is deprecated 2.8+ syntax. It silently does the wrong thing in some Blender versions and is rejected in others. Always use annotations.

See the rule type-annotate-props-and-defend-context for the rationale and the lint pattern.

The bpy.props types

Type Stores
BoolProperty One bool
IntProperty One signed int
FloatProperty One float
StringProperty A unicode string, optionally a subtype='FILE_PATH'
EnumProperty One choice from a fixed list, with optional options={'ENUM_FLAG'} for multi-select
BoolVectorProperty A fixed-size array of bools (size up to 32)
IntVectorProperty A fixed-size array of ints
FloatVectorProperty A fixed-size array of floats, often with subtype='COLOR' or subtype='TRANSLATION'
PointerProperty A reference to another PropertyGroup (or built-in datablock type)
CollectionProperty An ordered, named list of PropertyGroup instances

Use subtype to get UI semantics for free: 'COLOR' opens a color picker, 'FILE_PATH' opens a file dialog, 'TRANSLATION'/'XYZ'/'EULER' get the right unit display.

Where to store the data

Custom properties are bound to a Blender type as a class attribute:

bpy.types.Scene.my_addon = bpy.props.PointerProperty(type=MY_ADDON_PG_settings)

This makes bpy.data.scenes['Scene'].my_addon.intensity work for every Scene in the file.

The four conventional storage locations and their tradeoffs:

Location Persistence Scope Use for
bpy.types.Scene.x Saved with the .blend Per-Scene (per-document) Add-on state that should travel with the file (export settings, custom render passes, baked data references)
bpy.types.Object.x Saved with the .blend Per-Object Per-object metadata (constraint config, custom rig data, asset tags)
bpy.types.WindowManager.x Session-only, not saved Global to the running Blender Transient state (last used path, current modal step, temp UI flags)
AddonPreferences Saved with the user preferences Per-user, global API keys, default paths, "always do X" preferences

You can also bind to bpy.types.Mesh, bpy.types.Material, bpy.types.Armature, etc. for datablock-specific data.

Picking storage

  • Will the user expect to save and reopen the file with these values? Yes -> Scene or Object.
  • Per-document or per-object? Per-document -> Scene. Per-object -> Object.
  • User-global, like an API key? -> AddonPreferences.
  • Transient session state, like the current step of a wizard? -> WindowManager.

The PropertyGroup plus PointerProperty pattern

Group your add-on's settings into one PropertyGroup and bind that group via a single PointerProperty:

class MY_ADDON_PG_settings(bpy.types.PropertyGroup):
    intensity: bpy.props.FloatProperty(default=1.0)
    use_smoothing: bpy.props.BoolProperty(default=True)


def register():
    bpy.utils.register_class(MY_ADDON_PG_settings)
    bpy.types.Scene.my_addon = bpy.props.PointerProperty(type=MY_ADDON_PG_settings)


def unregister():
    del bpy.types.Scene.my_addon
    bpy.utils.unregister_class(MY_ADDON_PG_settings)

You then access:

scene = context.scene
scene.my_addon.intensity = 2.0

This is far cleaner than binding individual FloatPropertys directly to Scene. It also gives you a clear unregister target.

del first, then unregister_class

The order matters. The bound PointerProperty references the PropertyGroup class. If you unregister the class first, the binding becomes dangling and Blender crashes on next save. Always:

def unregister():
    del bpy.types.Scene.my_addon
    bpy.utils.unregister_class(MY_ADDON_PG_settings)

Unbinding a type-level property is del bpy.types.Scene.my_addon on all Blender versions. See the snippet cross-version-property-delete.py for removing a custom ID property (also del, version-stable).

CollectionProperty (lists)

class MY_ADDON_PG_item(bpy.types.PropertyGroup):
    name: bpy.props.StringProperty()
    enabled: bpy.props.BoolProperty(default=True)


class MY_ADDON_PG_settings(bpy.types.PropertyGroup):
    items: bpy.props.CollectionProperty(type=MY_ADDON_PG_item)
    active_index: bpy.props.IntProperty(default=0)

Add an item:

new_item = scene.my_addon.items.add()
new_item.name = "First"

Remove by index:

scene.my_addon.items.remove(0)

CollectionProperty plus a paired active_index: IntProperty is the canonical pattern for bpy.types.UIList integration.

AddonPreferences

For per-user, global data:

class MY_ADDON_AP_preferences(bpy.types.AddonPreferences):
    bl_idname = __package__  # the add-on's package name

    api_key: bpy.props.StringProperty(name="API Key", subtype='PASSWORD')
    default_export_path: bpy.props.StringProperty(name="Default Export Path", subtype='DIR_PATH')

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "api_key")
        layout.prop(self, "default_export_path")

Access via:

prefs = context.preferences.addons[__package__].preferences
api_key = prefs.api_key

AddonPreferences data persists in the user's preferences (userpref.blend), not the current document. Do not store per-document state here.

Why "just use Python attributes" does not work

# WRONG: this attribute does not survive save and load.
bpy.context.scene.my_intensity = 1.5

Properties attached as plain Python attributes to a Blender datablock are not serialized into the .blend file. The .blend file format only saves registered bpy.props. Reload the file and the value is gone.

This is a frequent AI mistake when generating quick scripts. Always wrap state in a PropertyGroup bound via PointerProperty if it must persist.

Common AI mistakes

  1. Assignment form for properties:

    class Bad(bpy.types.PropertyGroup):
        intensity = bpy.props.FloatProperty(default=0.5)  # silently broken

    See the rule type-annotate-props-and-defend-context.

  2. Storing complex state on Python attributes:

    scene.my_data = {"items": [...]}  # gone after save and reload
  3. Wrong unregister order (unregister class before deleting binding) -> crash on save.

  4. Binding individual properties directly to Scene instead of grouping into a PropertyGroup. Hard to unregister cleanly, pollutes the namespace.

  5. Storing per-document state in AddonPreferences -> data leaks across files.

  6. Confusing subtype='FILE_PATH' and 'DIR_PATH'. 'FILE_PATH' lets the user pick a file, 'DIR_PATH' a directory.

Compatibility paths (4.5 LTS vs 5.0+)

Two distinct operations, both version-stable via del:

  • Unbind a type-level property (e.g. a PointerProperty bound to bpy.types.Scene): del bpy.types.Scene.my_addon. Same on 4.5 LTS and 5.0+.
  • Remove a custom ID property (the dict-style obj["key"]): del obj["key"]. Same on 4.5 LTS and 5.0+. The snippet cross-version-property-delete.py shows this.

Do not reach for property_unset() here. It resets a registered RNA property to its default, which is neither unbinding a type property nor removing an ID property.

Related

  • addon-scaffolding for the registration boilerplate that wraps property registration
  • ui-panels for layout.prop reading these properties
  • Rule type-annotate-props-and-defend-context

References

Categories