Y = np.array([[1,2,3],[4,5,6]])
This creates a (2,3) array.
v = np.append(Y, [[7,8,9]], axis=0)
This is actually implemented as
v = np.concatenate(( Y, np.array([[7,8,9]]) ), axis=0)
That is, it joins a (2,3) array with (1,3) on axis 0 to make a (3,3).
q = np.append(Y,[[9],[10]], axis=1)
Here it's joining (2,3) with (2,1) on axis 1 to make (2,4)
np.concatenate assumes axis 0 unless you specify it. It does not attempt to guess which axis works or not. In the q case using axis 0 would result in an error.
Also concatenate requires matching number of dimensions, and match sizes on the non-concatenating axes. Try something like np.concatenate((Y, [2,1])) to see what I mean.
I don't like np.append since it looks too much like the list append, and gives beginners lots of problems. Without axis it ravels the inputs (read the docs and code if that isn't obvious). With axis it is just the same as concatenate (again read the code). concatenate takes a list of many arrays, not just 2. np.append artificially limits that action to 2.
np.vstack, np.hstack, and np.column_stack are variants that tweak the input dimensions in various ways, before calling concatenate. They compensate for some mis-matches, such as when joining rows:
np.vstack((Y, [7,8,9]))
turns the (3,) array into a (1,3) before concatenating. This is, in effect, an convenient and safe 'second-guessing'. It rarely gives problems like np.append.
numpy (and Python) tries to avoid ambiguities. It's more likely to raise an error than to second guess the user. And the trend in development is remove legacy cases where it allowed 'rule-breaking'.