Type error: expected a string or a tuple of strings

I tried making my script check if two objects have the same materials and if not remove the materials they don’t have in common:


for slot in object.material_slots:
                    if slot in bpy.data.objects[wanted_object_name].material_slots:
                        pass
                    else:
                        slot.material_slot_remove() 

“Object” is the one I want to remove materials from, “wanted_object_name” is a string with the name of an object.
But when I run it I get this:


TypeError: bpy_prop_collection.__contains__: expected a string or a tuple of strings


Any ideas? :confused:

The problem is not with wanted_object_name but with slot : you need to use its name (slot.name) in order to search into the material_slots collection, like so :

if slot.name in bpy.data.objects[wanted_object_name].material_slots:

I think you have to check if material slots actually exist first.


import bpy


wanted_object_name = "one"
object = bpy.data.objects.get("Cube")
for slot in object.material_slots:
    ob_check = bpy.data.objects[wanted_object_name]
    if ob_check != None:
        if len(ob_check.material_slots):
            if slot in ob_check.material_slots:
                pass
            else:
                slot.material_slot_remove()

You can’t test if a bpy.type instance is contained in a bpy_prop_collection by using the instance reference and the collection, you need to compare by name instead.

for slot in object.material_slots:
                    if slot.name not in (m.name for m in bpy.data.objects[wanted_object_name].material_slots):
                        slot.material_slot_remove()

Thanks for the replies, everyone! I have to do some homework now but I’ll check them when I can! :smiley:

Well, it’s true that I have to compare the names of the slots, but there’s an other problem: material_slot_remove doesn’t go with slot!
I have to do this:


object.active_material_index = mat_index
bpy.ops.object.material_slot_remove()

But how do I get the material index by name?

OK, here’s how I did it:


mats = []
                    
for slot in object.material_slots:
    for slot_dupl in bpy.data.objects[wanted_object_name].material_slots:
        if slot_dupl.name == slot.name:
            mats.append(slot_dupl.name)
        else:
            pass
                               
    for mat_index in range(len(object.material_slots)):
        if object.material_slots[mat_index].name in mats:
            pass
                            
        else:    
            bpy.context.scene.objects.active = object
            object.active_material_index = mat_index
            bpy.ops.object.material_slot_remove()

Worst lines of code in history, yeah!!! :RocknRoll:

you can do that smarter:

import bpy
from functools import reduce

obs = [ob for ob in bpy.context.selected_editable_objects if ob.type == 'MESH']
assert len(obs) > 1

common_mat_names = reduce(set.__and__, ({mat.name for mat in ob.data.materials if mat} for ob in obs))
print(common_mat_names)

for ob in obs:
    mats = ob.data.materials
    for i, mat in sorted(enumerate(mats), reverse=True):
        if not mat or mat.name not in common_mat_names:
            mats.pop(i)

Wow, you must know Python better that I do, cause I have problems understanding it…

That is quite pythonic, although there is still a nested for loop in there…

This list comprehension:

obs = [ob for ob in bpy.context.selected_editable_objects if ob.type == 'MESH']

is equivalent to:

obs = []
for ob in bpy.context.selected_editable_objects:
    if ob.type == 'MESH':
        obs.append(ob)

reduce(set.and, sequence) uses the given function to “combine” a sequence (here using &, which is like bitwise AND), like this:

{‘Mat_1’, ‘Mat_2’, ‘Mat_3’} & {‘Mat_1’, ‘Mat_3’} & {‘Mat_1’, ‘Mat_2’, ‘Mat_4’}

The steps:

{‘Mat_1’, ‘Mat_2’, ‘Mat_3’} & {‘Mat_1’, ‘Mat_3’} # = {‘Mat_1’, ‘Mat_3’}

{‘Mat_1’, ‘Mat_3’} & {‘Mat_1’, ‘Mat_2’, ‘Mat_4’} # = {‘Mat_1’}

Then the nested loops iterate over all selected mesh objects, and each of their materials. It removes all materials not being in the above set.
Because there is no materials.remove(), but only pop(), I use enumerate() to pop by index, and do that in reverse order as the indices change when you remove an item (and would result in wrong materials being removed or IndexErrors raised).

Hmmm, interesting stuff, I certainly have to look at that big Python tome more carefully…
Maybe today I’m gonna release an alpha version of my addon using my code, but I’m surely gonna study yours as well…
But I’m not really sure about readability, I sort of understand my code more clearly, but that could be the fact that I wrote it…
Anyway, thanks! :slight_smile: