Sythra

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.

vaibhavkothari· Aug 26, 2026· 9 min readIntermediate
ShareXLinkedIn
Matrix multiplication in NumPy: @ vs *, and the shape error everyone hits cover

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.

A2×3
123456
×
B3×2
789101112
=
C2×2
5864139154

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
1234
×
B
5678
=
A @ Bmatrix product
19224350
A
1234
·
B
5678
=
A * Belementwise
5122132
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 writeYou getUse for
A @ BMatrix productLinear algebra, neural network layers
np.matmul(A, B)Same as @Identical, older style
np.dot(A, B)Same for 2-DBehaves differently in higher dimensions
A * BElementwiseScaling, 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 over
  • 2 (dim 1) — the columns of the first
  • 3 (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:

A2×3
123456
A.T3×2
142536
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)

ProblemFix
shapes not alignedPrint both shapes; transpose one or swap the operands
Result is elementwise, not a productYou used *; use @
Expected 2D array, got 1Darr.reshape(-1, 1)
.T changes nothingThe array is 1-D; reshape instead
Broadcast error on additionReshape the vector to (-1, 1) or (1, -1)
Result shape looks wrongMultiplication is not commutative — A @ BB @ 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
  • .T is a no-op on 1-D arrays
  • reshape(-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.

Next reading on Sythra Articles

Keep reading

Related articles

Newsletter

Get new articles

Free essays on learning, Python, and ML — no account required. We’ll only email when there’s something worth reading.