Correct way to store an array in a custom property

I’ve done some searching here and on Google and haven’t found anything, I apologize if this has already been asked.

I have an Empty named “test” with a Custom Property, type Python. I want to store an array in this property and load the array when I open the file.

My array is called “light_rot_array” and my custom property is called “light_rot”, the code that assigns it is this line:
bpy.data.objects["test"]["light_rot"] = g.light_rot_array
This kind of works- it does save an array to the custom property, but the value of the custom property looks like this:
[<bpy id property array [3]>, <bpy id property array [3]>, <bpy id property array [3]>]
So it’s definitely not what I’m looking for. That array should look more like a 2D array of numbers. I know it’s something super simple I’m missing, I just don’t know what it is. Thanks for the help!

I think what’s happening here is that Blender automatically converts the values that you assign to custom properties into native blender types. It’s just that for most of those types (e.g. floats, strings, booleans, etc.), they are automatically converted back into python types when you access them.

You can actually get the original blender representation of any property by using the .path_resolve(coerce=False) method:

bpy.context.scene.cycles.path_resolve("samples", coerce=False)

Which will return a reference that shows <bpy_int, CyclesRenderSettings.preview_samples> when we convert it to a string using str(). (Make sure to set coerce to False in order to stop blender from converting it to a native python type)

The difference with arrays is that they cannot be added like normal (e.g. not using prop: ArrayProperty()), and cannot be drawn in the UI. As such, I think they’ve been a bit neglected, and they don’t return a native python list, instead keeping it as the blender <bpy id property array> type.
As such, you need to convert them to lists manually, either by using list(bpy_array), or by using the built-in bpy_array.to_list() method.

As a disclaimer, I don’t really know any of this for certain, and it’s just what I’ve kind of guessed by using the API a lot, and hopefully it answers your question.

Hope that helps!

1 Like

Sounds like a job for JSON ( or something similar ) ? Or, am I being naive?

1 Like

I tried using list() both when setting the custom property and getting from it. It didn’t seem to have an effect, I’m still getting [<bpy id property array [3]>].

I’m going to try the bpy_array.to_list() now, where do I import bpy_array from? I tried it and it gives me an error: bpy_array is not defined. I tried bpy.bpy_array but that’s not it

For sure. That’s my plan if I can’t get it working this way, is write it to JSON and save it to an external file. That’s super easy. I just would rather keep the data in Blender if possible

Ah, I should have been a bit clearer, you not only have one bpy_array, but a bunch of nested ones since you are storing vectors, aka, it is stored as a bpy_array containing lots of other bpy_arrays, and so you also have to convert those to python types as well, using something like this:

my_list = [array.to_list() for array in obj["light_rot"].to_list()]

This will not only convert the top array to a python list, but also the sub lists (aka the individual light rotation vectors)

Also, when I say bpy_array, what I really mean is the blender type that lists are converted to when they are stored. So you don’t have to import any module to use it, the to_list method is on every <bpy id property array> already.

But… JSON can be just a string , right?

If the custom data can be a string, is there a limit to it’s size?

Maybe I’m imagining a would be nice scenario… I’ll help with an add-on…

OH that makes more sense, thank you! That looks like it will work, I will try it right now :slight_smile:

You make a great point, I could just store the data as a string, since you can have a string custom property. My one hangup is that it would take an extra step to convert the string back to an array, and I’m trying to minimize overhead since this custom property is going to be updated fairly often.

2 Likes

So I tried [array.to_list() for array in bpy.data.objects["test"]["light_rot"].to_list()] and I’m getting an error:
AttributeError: ‘list’ object has no attribute ‘to_list’

It seems like Blender does think it’s a list instead of a bpy_array, which is annoying :confused:

I think I’m just going to with JSON, since it seems like Blender’s list comprehension is not very good. Thank you for your help though @Strike_Digital , I really appreciate it :slight_smile:

1 Like

hmm, that is weird, maybe try that bit without the to_list on the main list?

1 Like

I got it working with JSON, it’s a little weird going list > string > JSON > string for storage, but it gets the job done. Thanks again for your help!!

1 Like

Thanks man.

Code and data were, and apparently still are, my obsession. I just can’t help it.

(I dream in code),
Cal

1 Like

I love both, I’m with you, and I’ve definitely dreamed in code before :slight_smile: Your JSON idea worked perfectly, look at that beautiful data:

Hm, yeah, I just tried it out, and using

[array.to_list() for array in bpy.data.objects["test"]["light_rot"]]

Should work like you want.

The JSON idea is pretty smart as well! You could probably simplify the whole process by just using str(light_rot_list), which should be a bit faster than formatting it with JSON.
You can then use eval(the_string) to get the list back at the other end.

1 Like

I love when things should work but they don’t :sweat_smile: You’re smarter than me, it’s definitely something I’m doing wrong, but oh well. JSON works well enough.

You’re right, I’m a bit wary of using eval() but I might end up cleaning this all up and using that method :slight_smile:

1 Like

Nah, I’ve just spent longer banging my head against the brick wall that is the API sometimes :laughing:

Good luck with your addon anyway!

1 Like

The “officially supported” way to store arrays is with collection properties. You can tweak the values in the interface, and it should support undo / redo without a sweat.

import bpy

class VectorPropertyGroup(bpy.types.PropertyGroup):
    value: bpy.props.FloatVectorProperty(size=3)



class HelloWorldPanel(bpy.types.Panel):
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        layout = self.layout
        for entry in context.scene.my_vector_list:
            row = layout.row(align=True)
            row.label(text=entry.name)
            row.prop(entry, "value", text="")


def register():
    bpy.utils.register_class(HelloWorldPanel)
    bpy.utils.register_class(VectorPropertyGroup)
    bpy.types.Scene.my_vector_list = bpy.props.CollectionProperty(type=VectorPropertyGroup)


def unregister():
    bpy.utils.unregister_class(HelloWorldPanel)
    bpy.utils.register_class(VectorPropertyGroup)
    del bpy.types.Scene.my_vector_list

if __name__ == "__main__":
    register()
    
    bpy.context.scene.my_vector_list.add() 
    bpy.context.scene.my_vector_list[-1].name = "My Awesome first value"
    bpy.context.scene.my_vector_list[-1].value = (2, 0, 3)
    bpy.context.scene.my_vector_list.add() 
    bpy.context.scene.my_vector_list[-1].name = "My Awesome second value"
    bpy.context.scene.my_vector_list[-1].value = (-1, 5, 2)
    bpy.context.scene.my_vector_list.add() 
    bpy.context.scene.my_vector_list[-1].name = "My Awesome third value"
    bpy.context.scene.my_vector_list[-1].value = (3, 0, 7)

image

2 Likes

Thank you! This is another excellent option! I’m a bit old-fashioned and I really love the ease of JSON- especially if I want to export it to an CSV- but I do like the ease of editing your solution offers. I may at some point convert my pure JSON solution to use your UI. Right now the UI is on the backburner, as I have a lot of horribly complicated math to figure out first :thinking:

1 Like

I needed to store an python list with colours in the objects and this was my implementation.
In case it would be useful to anyone. This information is stored with the objects in the blender file, so it is persistent.

from bpy.utils import (
    register_class,
    unregister_class
)

class Colors(bpy.types.PropertyGroup):
    color: bpy.props.FloatVectorProperty(
        name='Color',
        size=4,
        precision=3,
        subtype='COLOR',
        min=0.0,
        max=1.0
    )


class ColorStack(bpy.types.PropertyGroup):
    name = 'ColosStack'

    # usage:
    #     C.object.color_stack.add_color([1, 1, 1, 1])
    #     C.object.color_stack.add_color([2, 2, 2, 1])
    #     C.object.color_stack.add_color([3, 3, 3, 1])
    #     C.object.color_stack.get_color(0)
    #     C.object.color_stack.get_last_color()
    #     C.object.color_stack.get_all_colors()

    colors: bpy.props.CollectionProperty(type=Colors)

    def add_color(self, a_c):
        if a_c != self.get_last_color():
            _item = self.colors.add()
            _item.color = a_c

    def get_len_colors(self):
        return len(self.colors)

    def is_void(self):
        if self.get_len_colors() == 0:
            return True
        return False

    def overwrite_first_color(self, color: list):
        if self.get_len_colors() > 0:
            self.colors[0].color = color

    def get_color(self, index: int):
        if self.get_len_colors() > 0:
            color = self.colors[index].color
            return list(color)
        return None

    def get_last_color(self):
        if self.get_len_colors() > 0:
            last_color = self.colors[-1].color
            return list(last_color)
        return None

    def get_first_color(self):
        if self.get_len_colors() > 0:
            first_color = self.colors[0].color
            return list(first_color)
        return None

    def get_all_colors(self):
        if self.get_len_colors() > 0:
            tmp_a = []
            for col in self.colors:
                color = list(col.color)
                tmp_a.append(color)
            return list(tmp_a)
        return None

    def rm_color(self, color: list):
        if self.is_void():
            return None
        colors = self.get_all_colors()
        if color not in colors:
            return None
        idx = colors.index(color)
        self.colors.remove(idx)

    def rm_by_index_color(self, index: int):
        if self.is_void():
            return None
        self.colors.remove(index)

    def rm_last_color(self):
        if self.is_void():
            return None
        self.colors.remove(len(self.colors)-1)

def register():
    register_class(Colors)
    register_class(ColorStack)
    bpy.types.Object.color_data = bpy.props.CollectionProperty(type=Colors)
    bpy.types.Object.color_stack = bpy.props.PointerProperty(type=ColorStack)

def unregister():
    del bpy.types.Object.color_data
    del bpy.types.Object.color_stack
    unregister_class(Colors)
    unregister_class(ColorStack)
3 Likes

Note in python class names should be CamelCase by convention and you can store colors as an array of Floats with

color: bpy.props.FloatVectorProperty(size=3)

https://docs.blender.org/api/current/bpy.props.html?highlight=floatvectorproperty#bpy.props.FloatVectorProperty
that way you don’t have to parse from and to string.

2 Likes

Thank you very much, I have already updated it!