Element-wise * vs Matrix @
The single most important distinction in NumPy linear algebra: * multiplies position by position, while np.dot, @ and np.matmul perform matrix multiplication.
Until now, every multiplication we did was element-wise: runs * 2, runs / balls. Linear algebra introduces a completely different operation that also "multiplies" two arrays — matrix multiplication. Mixing the two up is one of the most common and most serious bugs in data-science code, because both often run without any error and simply give wrong numbers.
Definition
A matrix is a 2D array of numbers arranged in rows and columns. A vector is a 1D array.
Element-wise multiplication (
A * B) multiplies the values at the same position in both arrays. The arrays must have the same shape (or be broadcastable), and the result has that same shape.
Matrix multiplication (
A @ B,np.matmul(A, B),np.dot(A, B)) combines each row of A with each column of B by multiplying matching entries and adding them up. It needs the number of columns of A to equal the number of rows of B, and the result usually has a different shape.
- Same position —
(A * B)[i, j] = A[i, j] * B[i, j]. Nothing else is involved. - Row with column —
(A @ B)[i, j]uses the entire rowiof A and the entire columnjof B. @— the matrix-multiplication operator, added in Python 3.5 specifically for NumPy. It is the modern, preferred spelling.
Syntax
python
A * B # element-wise product
A @ B # matrix product (preferred)
np.matmul(A, B) # same as A @ B
np.dot(A, B) # same as A @ B for 1D and 2D arrays| Function / operator | Parameters | Meaning |
|---|---|---|
A * B | two arrays (or array and scalar) | product of matching positions, broadcasting allowed |
A @ B / np.matmul(A, B) | two arrays, no scalars | matrix product; stacks of matrices for 3D+ |
np.dot(a, b) | two arrays or scalars | dot product (1D), matrix product (2D), scalar multiply if one is a number |
Example — same two matrices, two very different answers
python
import numpy as np
X = np.array([[1, 2],
[3, 4]])
Y = np.array([[5, 6],
[7, 8]])
print("X * Y (element-wise):")
print(X * Y)
print("X @ Y (matrix multiplication):")
print(X @ Y)text
X * Y (element-wise):
[[ 5 12]
[21 32]]
X @ Y (matrix multiplication):
[[19 22]
[43 50]]How it works:
text
X * Y (position by position) X @ Y (row · column)
[0,0] = 1*5 = 5 [0,0] = 1*5 + 2*7 = 19
[0,1] = 2*6 = 12 [0,1] = 1*6 + 2*8 = 22
[1,0] = 3*7 = 21 [1,0] = 3*5 + 4*7 = 43
[1,1] = 4*8 = 32 [1,1] = 3*6 + 4*8 = 50Example — np.dot, @ and np.matmul agree for 2D arrays
python
A = np.array([[1, 2, 3],
[4, 5, 6]])
B = np.array([[7, 8],
[9, 10],
[11, 12]])
print(np.dot(A, B))
print(A @ B)
print(np.matmul(A, B))
print(np.array_equal(A @ B, np.dot(A, B)), np.array_equal(A @ B, np.matmul(A, B)))text
[[ 58 64]
[139 154]]
[[ 58 64]
[139 154]]
[[ 58 64]
[139 154]]
True TrueA (2, 3) matrix times a (3, 2) matrix gives a (2, 2) result — a different shape from both inputs. Element-wise multiplication of these two is not even possible:
python
try:
A * B
except ValueError as e:
print("ValueError:", e)text
ValueError: operands could not be broadcast together with shapes (2,3) (3,2)Comparison — * vs @ vs np.dot vs np.matmul
* | @ | np.matmul | np.dot | |
|---|---|---|---|---|
| Operation | element-wise | matrix product | matrix product | dot / matrix product |
| Shape rule | same shape or broadcastable | (m,n) @ (n,p) | (m,n) @ (n,p) | (m,n) · (n,p) |
| Result shape (2D) | same as inputs | (m, p) | (m, p) | (m, p) |
| Scalar allowed? | yes (A * 2) | no | no | yes (acts like *) |
| 1D · 1D | element-wise array | single number | single number | single number |
| 3D+ arrays | element-wise | batch of matrix products | batch of matrix products | different (sum-product over last/second-last axes) |
| When to use | scaling, per-cell maths | default for matrices | same as @, function form | old code, vector dot products |
Note: for 1D and 2D arrays
np.dot,@andnp.matmulgive identical results. They only differ for scalars and 3D+ arrays — prefer@and you will never hit those surprises.
Common mistakes
- ❌ Using
*when you meant a weighted sum / matrix product — square matrices of equal shape give no error, just wrong numbers. ✅ Use@whenever you want row-times-column sums. - ❌
A @ 2→ValueError: matmul: Input operand 1 does not have enough dimensions .... ✅ Scale withA * 2. - ❌ Thinking
A @ B == B @ A. ✅ Matrix multiplication is not commutative: the order changes the answer (and often the shape). - ❌ Coming from MATLAB and expecting
*to be matrix multiplication. ✅ In NumPy*is always element-wise.
Interview tip
Q: What is the difference between np.dot, np.matmul and *?
A: * is element-wise multiplication. np.matmul (and @) is matrix multiplication and treats 3D+ arrays as stacks of matrices, but rejects scalars. np.dot is identical to matmul for 1D/2D arrays, also accepts scalars, and behaves differently for higher-dimensional arrays.
Key points to remember
- A matrix is a 2D array; a vector is a 1D array.
- A * B multiplies values at the same position; shapes must match (or broadcast).
- A @ B multiplies rows of A with columns of B and sums — true matrix multiplication.
- For 2D arrays np.dot(A, B), A @ B and np.matmul(A, B) give identical results.
- @ and matmul reject scalars; np.dot and * accept them.
- Matrix multiplication is not commutative: A @ B is usually not B @ A.
- Prefer @ for matrix products in modern code.
Try it yourself
* vs @ on the same matrices
Three spellings, one result
Practice: spot the bug
Revision Flashcards
Tap a card to flip it and see the answer.
