Trying to understand coordinate spaces in python

In a sense I have "solved" my original problem but I don't understand why/how its solved.

I have a script that generates a number of objects and then attempts to create straight lines via a Nurbs path, adapted from the question/answer here. Using Hooks is important because I want the 'lines' between objects to persist in relative terms to their parents.

The solution I've found is that if I create an otherwise unused empty object with defined coordinates (uncomment out line 52 bpy.ops.object.empty_add(location=list(objA.location),)) everything works as expected. If I remove this hack-ish work around though, the coordinates for the ends of the nurbs paths are all translated/scaled which is not the desired behavior.

I thought the "answer" might be related to object vs world space (and maybe it still is?) or maybe a misunderstanding of context, but this hail-Mary work around of mine worked so thought I'd quit while I was ahead/ask a more specific question: "why does this 'hack' work?" and perhaps "is there a better way?"

Code example showing the undesirable behavior (uncomment out line 52 to see correct behavior)

#names and locations used to drive example
pointDict ={'obj_0': (3.453742273151139, 0.5159551844564874,0.0),
'obj_1': (12.792816730076403, -16.205364756872424,0.0),
'obj_2': (14.709455272016719, -11.60690199900985,0.0),
'obj_3': (8.69197652556564, -13.612261105632582,0.0),
'obj_4': (9.52728618428947, 2.4890328748868917,0.0)}

import bpy
C = bpy.context
from itertools import combinations

#template cube to clone, used for minimum viable example
bpy.ops.mesh.primitive_cube_add(size=0.5, align='WORLD', location=(10.0,10.0,0.0), scale=(1, 1, 1))

baseCube = bpy.data.objects['Cube'] #known/assumed name

#clone base object with predetermined coordiantes
for keyName in pointDict:
xVar, yVar, zVar = pointDict[keyName]
objClone = baseCube.copy()
objClone.name = keyName
objClone.location.x = xVar
objClone.location.y = yVar
objClone.location.z = zVar

#create pair-wise line combinations between cubes
#and hook to cubes to maintain position/connections when moved at later point
pCounter = 0
for pair in combinations(list(pointDict.keys()),2):
pCounter=+1

objA = bpy.data.objects[pair[0]]
objB = bpy.data.objects[pair[1]]

path = bpy.data.curves.new('_path_{}'.format(pCounter), 'CURVE')
path.dimensions = "3D"
path.bevel_depth=0.05

spline = path.splines.new(type='NURBS')
spline.use_endpoint_u = True
spline.use_endpoint_v = True
path_ob = bpy.data.objects.new('_Path_new_{}'.format(pCounter), path)

spline.points[0].co = (list(objA.location)+[1.0])

#not sure why adding this helps, but it fixes things??  uncoment following line to see "good" results
hm1 = path_ob.modifiers.new(name=f"Hook_A",type='HOOK',)
hm1.object = objA
hm1.vertex_indices_set([0])
#'Hook' first vertex of spline/path
spline.points[1].co = (list(objB.location)+[1.0])
hm2 = path_ob.modifiers.new(name=f"Hook_B",type='HOOK',)
hm2.object = objB
hm2.vertex_indices_set([1])



Force an update.

Using the add empty object operator (or any operator) has the side effect of forcing a view_layer update. In version < 2.8 this was a scene.update().

It can be the case in a script execution that object matrices are not yet updated when their values are being queried.

Updating the viewlayer in the same loop as the operator produces the same result as with operator, without operator. (circa line 30)

pCounter = 0
for pair in combinations(list(pointDict.keys()),2):
pCounter = +1 # Did  you mean += 1
bpy.context.view_layer.update()  # <== UPDATE
objA = bpy.data.objects[pair[0]]