Under the hood this `push!` function have to do memory reallocation and copying the entire array when the array grows to be larger than some internal buffer, which happens each time the array becomes larger than a^n, where a is usually, but not necessary, selected to be equal to 2.
The amortized complexity is θ(1), but individual insertions might take O(n) time.
It means that it is not hard to have the same in numpy, with the same performance, the only difference is that the growing have to be done manually and that the push_back function has to be called as something like
x, x_len = push_back(x, x_len, 10)
with push_back being defined like
def push_back(x, x_len, value):
if len(x) == x_len:
x = np.concatenate((x, np.empty(max(len(x), 1)))
x[x_len] = value
return x, x_len + 1
so that the x variable is supposed to be updated after each insertion.