Ask yourself when np.zeros(1) is called in both cases. Alternatively, run this code:
def printingFn():
print("printingFn")
return 42
def func1(x = printingFn()):
print("func1")
def func2(x = None):
if x is None: x = printingFn()
print("func2")
func1()
func1()
func1()
print()
func2()
func2()
func2()
You'll see that only one call is made to printingFn in the func1 sequence, because the default argument is calculated when the function is defined, not every time it is called:
printingFn
func1
func1
func1
printingFn
func2
printingFn
func2
printingFn
func2
That's also true of the func2 sequence, in that the default argument of None is evaluated at function definition. However, the code that calls printingFn, since the argument is None, happens on each call.
What that means in your case is that the good_function variant of x is being created freshly every time the function is called. The bad_function variant is hanging around so maintains the changes made when you assign something to x[0].