---
title: "Matrix multiplication in NumPy: @ vs *, and the shape error everyone hits"
description: "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."
url: https://articles.sythra.ai/articles/numpy-matrix-multiplication-shapes
slug: numpy-matrix-multiplication-shapes
author: "vaibhavkothari"
author_url: https://articles.sythra.ai/writers/vaibhavkothari
date_published: 2026-08-26T13:46:51.908Z
date_modified: 2026-08-29T10:29:25.265Z
topics: ["Python", "Machine Learning", "Numpy"]
reading_time_minutes: 9
publisher: "Sythra"
publisher_url: https://sythra.ai
access: free
language: en
---

# 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.

Source: https://articles.sythra.ai/articles/numpy-matrix-multiplication-shapes · Author: vaibhavkothari · Published: 2026-08-26 · Reading time: 9 min · Topics: Python, Machine Learning, Numpy

Two operators. Two completely different results. One very common error message.

```text
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0
```

or the older wording:

```text
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.

```text
(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.

```matrix
# A (2×3)
[1] [2] [3]
4 5 6
×
# B (3×2)
[7] 8
[9] 10
[11] 12
=
# C (2×2)
[58] 64
139 154
```

The highlighted cells produce the highlighted result:

```text
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.

```python
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.

```matrix
# A
1 2
3 4
×
# B
5 6
7 8
=
# A @ B  (matrix product)
19 22
43 50
```

```matrix
# A
1 2
3 4
·
# B
5 6
7 8
=
# A * B  (elementwise)
5 12
21 32
```

```python
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

```text
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:

```python
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:

```matrix
# A (2×3)
1 2 3
4 5 6
→
# A.T (3×2)
1 4
2 5
3 6
```

```python
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 matrix 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:

```python
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:

```python
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:

```python
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:

```python
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:

```text
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:

```python
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.

```text
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:

```python
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:

```python
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
- `.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

```remember
# Remember this
`A @ B` -> matrix product, rows meet columns
`A * B` -> elementwise, same position times same position
`(m×n) @ (n×p)` -> `(m×p)`; the inner numbers must match
---
`.T` does nothing to a 1-D array. Use `reshape(-1, 1)` for a real column.
```


## 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

- [What matrices actually do in machine learning](https://articles.sythra.ai/articles/matrix-operations-for-machine-learning)
- [Train your first ML model in 20 lines](https://articles.sythra.ai/articles/first-machine-learning-model-scikit-learn)
- [Stop memorizing Python syntax](https://articles.sythra.ai/articles/seven-python-patterns-for-ml-notebooks)

## Glossary (terms defined in this article)
- **covariance matrix** (advanced) — A table showing how each pair of features varies together
