Article
Matrix multiplication in NumPy: @ vs *, and the shape error everyone hits
Why A @ B and A * B give completely different answers, what "shapes not aligned" really means, and the one rule that makes matrix shapes click for good.
On this page0%
- The one rule
- Step 1 — See what multiplication actually does
- Step 2 — Know the difference between @ and *
- Step 3 — Read the error message
- Step 4 — Transpose when the shapes are simply flipped
- Step 5 — Handle the (n,) versus (n,1) trap
- Step 6 — Use broadcasting instead of loops
- Step 7 — Where this shows up in machine learning
- Common problems (and fixes)
- What you learned
- FAQ
- What is the difference between @ and * in NumPy?
- What does "shapes not aligned" mean in NumPy?
- How do I know the shape of a matrix product?
- Why does .T not transpose my array?
- Is matrix multiplication commutative?
- Next reading on Sythra Articles
Two operators. Two completely different results. One very common error message.
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0
or the older wording:
ValueError: shapes (3,2) and (3,2) not aligned: 2 (dim 1) != 3 (dim 0)
Both say the same thing: the inner numbers of your two shapes do not match. Once you can read that sentence, this error stops being scary and starts being useful.
The one rule
To multiply two matrices, the inner dimensions must be equal. The result takes the outer ones.
(2, 3) @ (3, 4) -> (2, 4)
^ ^
these must match
^ ^
these become the result
Say it out loud once: "two by three times three by four gives two by four." That is the whole rule. Everything below is a consequence of it.
Step 1 — See what multiplication actually does
Each output cell is one row met with one column: multiply pairwise, add the results.
The highlighted cells produce the highlighted result:
1×7 + 2×9 + 3×11 = 7 + 18 + 33 = 58
That is why the inner dimensions must match — row A has 3 numbers, so column B needs exactly 3 numbers to pair with. And it is why the result is 2×4-shaped in general: one output cell per (row of A, column of B) pair.
import numpy as np
A = np.array([[1, 2, 3],
[4, 5, 6]])
B = np.array([[7, 8],
[9, 10],
[11, 12]])
print(A.shape, B.shape) # (2, 3) (3, 2)
print(A @ B)
# [[ 58 64]
# [139 154]]
Step 2 — Know the difference between @ and *
This is the single most common source of silently wrong results, because * often runs without error and gives you a plausible-looking array.
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(A @ B) # [[19 22] [43 50]] rows meet columns
print(A * B) # [[ 5 12] [21 32]] same position × same position
| You write | You get | Use for |
|---|---|---|
A @ B | Matrix product | Linear algebra, neural network layers |
np.matmul(A, B) | Same as @ | Identical, older style |
np.dot(A, B) | Same for 2-D | Behaves differently in higher dimensions |
A * B | Elementwise | Scaling, masks, weighting |
np.multiply(A, B) | Same as * | Explicit elementwise |
Rule of thumb: if you meant "combine rows with columns", you want @. If you meant "multiply matching cells", you want *.
Step 3 — Read the error message
ValueError: shapes (3,2) and (3,2) not aligned: 2 (dim 1) != 3 (dim 0)
Translate it piece by piece:
shapes (3,2) and (3,2)— what you handed over2 (dim 1)— the columns of the first3 (dim 0)— the rows of the second!=— they must be equal, and they are not
So the fix is either to transpose one of them, or to accept you had the operands the wrong way round. Print the shapes first, every time:
print("A:", A.shape, "B:", B.shape)
That one line resolves the majority of these errors before you have to think.
Step 4 — Transpose when the shapes are simply flipped
.T swaps rows and columns:
A = np.array([[1, 2, 3], [4, 5, 6]]) # (2, 3)
B = np.array([[1, 2, 3], [4, 5, 6]]) # (2, 3)
# A @ B -> ValueError: 3 != 2
print((A @ B.T).shape) # (2, 2)
print((A.T @ B).shape) # (3, 3)
Both are valid, and they mean different things. A @ B.T compares rows with rows. A.T @ B compares columns with columns — that second one is exactly how a covariance matrixA table showing how each pair of features varies together is computed. Choose based on what you want to compare, not on which one stops the error.
Note that .T does nothing to a 1-D array. This surprises everyone once:
v = np.array([1, 2, 3])
print(v.shape, v.T.shape) # (3,) (3,) -- unchanged
Step 5 — Handle the (n,) versus (n,1) trap
NumPy has three things that all look like "a column of numbers" and behave differently:
a = np.array([1, 2, 3]) # (3,) 1-D, neither row nor column
b = np.array([[1, 2, 3]]) # (1, 3) row vector
c = np.array([[1], [2], [3]]) # (3, 1) column vector
The 1-D array is flexible — NumPy treats it as whichever orientation makes the multiplication work:
M = np.array([[1, 2, 3], [4, 5, 6]]) # (2, 3)
print((M @ a).shape) # (2,) treated as a column
print((a @ M.T).shape) # (2,) treated as a row
Convenient, until it silently hides a bug. Reshape when you want to be explicit:
column = a.reshape(-1, 1) # (3, 1)
row = a.reshape(1, -1) # (1, 3)
-1 means "work this dimension out from the total size". This is also the fix for scikit-learn's familiar complaint:
Expected 2D array, got 1D array instead. Reshape your data using array.reshape(-1, 1)
Step 6 — Use broadcasting instead of loops
Broadcasting stretches a smaller array across a bigger one when the shapes are compatible, with no copying:
X = np.array([[1, 2, 3],
[4, 5, 6]]) # (2, 3)
bias = np.array([10, 20, 30]) # (3,)
print(X + bias)
# [[11 22 33]
# [14 25 36]]
The rule: compare shapes from the right; dimensions must be equal, or one of them must be 1.
X (2, 3)
bias (3,) -> treated as (1, 3) -> stretched to (2, 3) OK
X (2, 3)
b (2,) -> treated as (1, 2) -> 3 vs 2 FAILS
To add one value per row rather than per column, make the intent explicit:
row_bias = np.array([100, 200]).reshape(-1, 1) # (2, 1)
print(X + row_bias)
# [[101 102 103]
# [204 205 206]]
Step 7 — Where this shows up in machine learning
A neural network layer is one matrix multiply and one addition:
batch = 32
features = 10
units = 4
X = np.random.randn(batch, features) # (32, 10)
W = np.random.randn(features, units) # (10, 4)
b = np.zeros(units) # (4,)
out = X @ W + b # (32, 4)
print(out.shape)
Read the shapes as a sentence: 32 samples with 10 features each, through a layer that maps 10 features to 4, gives 32 samples with 4 outputs. The middle number cancels out — 10 meets 10 and disappears. That is the shape rule doing all the work.
Get W the wrong way round and you get the shape error, not a wrong answer. Shape errors are the friendly kind of bug: they fail immediately instead of silently training something meaningless — unlike using * where you meant @.
Common problems (and fixes)
| Problem | Fix |
|---|---|
shapes not aligned | Print both shapes; transpose one or swap the operands |
| Result is elementwise, not a product | You used *; use @ |
Expected 2D array, got 1D | arr.reshape(-1, 1) |
.T changes nothing | The array is 1-D; reshape instead |
| Broadcast error on addition | Reshape the vector to (-1, 1) or (1, -1) |
| Result shape looks wrong | Multiplication is not commutative — A @ B ≠ B @ A |
What you learned
- Inner dimensions must match; outer dimensions become the result
@combines rows with columns;*multiplies matching cells- Every shape error names the two numbers that disagree
.Tis a no-op on 1-D arraysreshape(-1, 1)converts a flat array to a real column- Broadcasting compares shapes from the right
FAQ
What is the difference between @ and * in NumPy?
@ performs matrix multiplication, combining the rows of the first array with the columns of the second. * multiplies elements in matching positions and requires the shapes to match or broadcast.
What does "shapes not aligned" mean in NumPy?
The columns of the first array do not equal the rows of the second. The message names both numbers — transpose one array or swap the operands so the inner dimensions match.
How do I know the shape of a matrix product?
Take the outer dimensions. (2, 3) @ (3, 4) produces (2, 4); the shared inner dimension of 3 disappears.
Why does .T not transpose my array?
The array is one-dimensional, with shape (n,), which has no rows or columns to swap. Use reshape(-1, 1) for a column or reshape(1, -1) for a row.
Is matrix multiplication commutative?
No. A @ B and B @ A usually differ, and often only one of them is even a valid shape.