Unnecessary temporary arrays is definitely a major source of inefficiency when working with NumPy, but recent versions of NumPy go to heroic lengths (via Python reference counting) to avoid doing so in many cases:
https://github.com/numpy/numpy/blob/v1.18.3/numpy/core/src/m...
So in this case, NumPy would actually only make one temporary copy, effectively translating the loop into the following:
for j in range(255):
u = z**2 # create a new squared array
u += c # add in-place
z = u # replace the old array
So in this case, NumPy would actually only make one temporary copy, effectively translating the loop into the following: