I am trying to make a script that works kind of the same as Merge by Distance but to be used in Object mode. Basically, what I am trying to do is join all the objects that have their origin at the same location. This is the script I tried to use :

import bpy;

visible_objects=[ob for ob in bpy.context.view_layer.objects if ob.visible_get()];
#get all the visible objects in a list

for o in visible_objects: #iterate the objects
    bpy.context.view_layer.objects.active= o; #set the current one as active
    for j in visible_objects: #iterate again
        if j== o:
        if j.location== o.location: #join the objects sharing location with the current one

There are 2 problems with this code.

The first one is a logical one, once an object is joined to another, it "disappears" but the visible_objects list is still referencing it so I get the obvious error : "ReferenceError: StructRNA of type Object has been removed".

The second problem is that I keep receiving the warning "Warning: Active object is not a selected mesh" after each join even though I already set an object as active with bpy.context.view_layer.objects.active= o and all the other objects are meshes.

Is there a way to get rid of the warning and how do I fix the algorithm so that the inexistant objects are not anymore referenced by the list?


2 Answers 2

import bpy

dic_obj_loc = {ob : tuple(ob.location) for ob in bpy.context.view_layer.objects if ob.visible_get() and ob.type == 'MESH'}

# get same location dict
rev_multidict = {}
for k, v in dic_obj_loc.items():
    rev_multidict.setdefault(v, set()).add(k)

for k, objs in rev_multidict.items():
    print(k, objs)
    if len(objs) == 1: continue

    for obj in objs:

    bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
  • $\begingroup$ Thank you for the answer. Can you explain what this line does tho? rev_multidict.setdefault(v, set()).add(k). I know it adds k to rev_multidict but I don't understrand what setdefault(v, set()) does. $\endgroup$ Commented Sep 21, 2022 at 10:28
  • 1
    $\begingroup$ @mqbakamqbaka it's better to use defaultdict instead, setdefault reads a value from a dictionary. If no value is found, it sets the value to the - in this case - set(), then it reads the value again, so now it has either a reference to the new set just created, or a reference to a set that was already there. $\endgroup$ Commented Sep 21, 2022 at 10:30
  • 1
    $\begingroup$ also, vectors have a method to_tuple - I don't know if it's faster, but should be otherwise there's no reason for it to be there 🤔 $\endgroup$ Commented Sep 21, 2022 at 10:32
  • $\begingroup$ Thanks for the explanation. My blender froze after I tried the script tho :'D $\endgroup$ Commented Sep 21, 2022 at 10:37
  • $\begingroup$ So it seems like Blender is still processing the script now, I just checked system console and the prints are still going on but the main UI turned unresponsive. $\endgroup$ Commented Sep 21, 2022 at 10:42

Another solution which uses a context override which will prevent blender from freezing too much because the selection states takes a lot of time to process when there are a lot of objects in the scene :

import bpy
from collections import defaultdict
dic = defaultdict(set)
for obj in bpy.data.objects: 
for objects in dic.values():
    if len(objects) <= 1:
    objects = list(objects)
    with bpy.context.temp_override(

Link to the operator override arguments


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .