Worked examples — NumPy broadcasting rules
You have read the parent note and know the three rules. This page is a drill hall. We line up every kind of shape pairing NumPy can throw at you, then walk each one to the ground so that no shape combination can ever surprise you again.
Before we start, one picture and one reminder to fix everything we will use on every single example.

Look at the figure: two shape tuples are printed as train carriages, pushed flush against a right-hand wall. The shorter train gets empty carriages (each holding a 1) welded onto its left — that welding is Rule 3. The three coloured verdicts — magenta "equal" (Rule 1), violet "one is 1 → stretch" (Rule 2), orange "clash → ValueError" — are the only three things that can happen to a carriage pair. That is the entire game.
The scenario matrix
Every broadcasting question you will ever meet is one row of this table. The examples below are numbered to hit each cell.
| # | Case class | What makes it special | Covered by |
|---|---|---|---|
| A | Scalar vs anything | one array has rank 0, shape () |
Ex 1 |
| B | Equal shapes | no stretching at all — pure element-wise | Ex 2 |
| C | Singleton in trailing axis | one array has a 1 on the right |
Ex 3 |
| D | Both sides stretch (row × col) | (1,n) meets (m,1) → grid |
Ex 4 |
| E | Rank mismatch (missing axes) | short array padded on the left | Ex 5 |
| F | Degenerate: a 0-length axis |
an axis of size 0 | Ex 6 |
| G | Hard failure | trailing axes clash, no 1 to save it |
Ex 7 |
| H | Real-world word problem | feature normalization on a data table | Ex 8 |
| I | Exam twist: transpose trap | right-alignment lands on the wrong axis | Ex 9 |
| J | Rank > 2 (N-dimensional) | a 3-D batch meets a 2-D operand | Ex 10 |
We build each one from the shape tuple, never from intuition alone. Intuition lies; the right-alignment picture does not.
The examples
- Write the shapes.
Ais(2, 3),bis(). Why this step? Because broadcasting is decided only by shapes, never by the numbers inside. Always read the tuple first. - Right-align and pad.
bhas rank 0,Ahas rank 2, sobgets two padding1s on the left → effective(1, 1). Why this step? Rule 3 inserts a size-1axis for each axisblacks (it lacks both), and padding goes on the left because NumPy compares from the right. - Compare per axis. Trailing:
3 vs 1→1stretches to3. Next:2 vs 1→1stretches to2. Why this step? Each padded1is now an ordinary singleton, so Rule 2 stretches it to match its partner — this is the "Rule 3 then Rule 2" hand-off in action. - Result shape =
(max(2,1), max(3,1)) = (2, 3). Why this step? By the output rule, each axis takes the larger of the two sizes; since everyb-axis is 1,Asets the whole shape. Every element gets10added, soresult[1, 2] = 6 + 10 = 16.
Verify: A had a 6 at [1,2]; add 10 → 16. Shape unchanged because a scalar cannot enlarge anything. ✓
- Shapes: both
(2, 2). Why this step? Equal ranks, so Rule 3 never fires — every carriage already has a partner, no padding needed. - Compare:
2 vs 2(equal),2 vs 2(equal) → Rule 1 on both axes. Why this step? When both sizes match, Rule 1 passes the size through untouched, so nothing is stretched; this is ordinary element-wise multiplication. - Multiply element by element: Why this step? With no stretching anywhere, each output cell is simply the product of the two same-position inputs — the plain definition of an element-wise operation. (the symbol just means "multiply matching cells", not matrix multiplication).
Verify: Position [1,0]: . Position [1,1]: . ✓ No broadcasting happened — this is your control case, the "boring" baseline everything else is measured against.
- Shapes:
Xis(3, 3),meanis(3, 1). Same rank, no padding. Why this step? Both are rank 2, so Rule 3 stays quiet — we go straight to comparing sizes from the right. - Trailing axis:
3 vs 1→ Rule 2, the1stretches to3. Why this step?meanis one column; Rule 2 virtually copies that single column across all 3 columns ofX. No memory is duplicated. - Next axis:
3 vs 3(equal) → Rule 1, stays 3. Result shape(max(3,3), max(3,1)) = (3, 3). Why this step? The row axis already matches, so Rule 1 leaves it alone; the output takes the larger size on each axis, giving(3, 3). - Subtract: each row of
Xloses its own row-mean. Row 2 (index 2) is[7,8,9] - 8 = [-1, 0, 1]. Why this step? After the stretch,mean's value8sits in all three columns of row 2, so every entry of that row has8subtracted.

Look at the figure: the single violet column of mean fans out into three faded copies (the virtual stretch) so it can meet every cell of the orange X grid.
Verify: Row 0: [1,2,3]-2 = [-1,0,1]. Row 1: [4,5,6]-5=[-1,0,1]. Row 2: [7,8,9]-8=[-1,0,1]. Every row centers to [-1,0,1] — a clean sanity check that our subtraction hit the right axis. ✓
- Reshape
a.a[:, np.newaxis]inserts a new trailing axis → shape(3, 1). Why this step? We want every pairwise product. To force a 2-D grid, one operand must own the rows and the other the columns. - Right-align
b.bis(4,), rank 1; Rule 3 pads it on the left → effective(1, 4). Why this step? Rank mismatch:(3,1)has rank 2,bhas rank 1, so Rule 3 inserts one size-1axis onb's left. - Compare axes. Trailing:
1 vs 4→ Rule 2,astretches to 4. Next:3 vs 1→ Rule 2,bstretches to 3. Why this step? This is the double-stretch: both operands have a singleton, so Rule 2 fires on each axis in opposite directions, producing an all-pairs grid — the definition of an outer product. - Result
(max(3,1), max(1,4)) = (3, 4). Cell[i,j] = a[i] * b[j]. Row 2, col 3 =3 * 40 = 120. Why this step? Each axis takes the larger size, so the grid is3rows by4columns; after both stretches, position[i,j]holdsa[i](from the stretched column) timesb[j](from the stretched row).

The figure shows the magenta column a and the orange row b each fanning out over the empty grid; where the fans overlap, the product is written in navy.
Verify: Full grid:
[[ 10 20 30 40]
[ 20 40 60 80]
[ 30 60 90 120]]
Bottom-right corner 3*40 = 120. ✓
- Shapes:
Mis(2, 3),vis(3,). Why this step? Different ranks (2 vs 1) means Rule 3 will fire — but we must first know which axis is missing, and it is always the leftmost. - Right-align. Rule 3 gives
vone size-1axis on the left → effective(1, 3). Why this step? The pad always lands on the left, sov's single3sits underM's trailing3— under the columns, not the rows. - Compare. Trailing:
3 vs 3→ Rule 1 (equal). Next:2 vs 1→ Rule 2,vstretches to 2 rows. Why this step? The column axis already matches (Rule 1), and the padded1on the row axis is stretched (Rule 2) sovis copied onto both rows — the same vector added to every row. - Result
(2, 3): row 0 =[0,1,2]+[10,20,30] = [10,21,32]; row 1 =[3,4,5]+[10,20,30] = [13,24,35]. Why this step? Withvnow present in both rows, we simply add the aligned column entries position by position.
Verify: Corner [1,2] = 5 + 30 = 35. This is the "add-a-bias-row" pattern used everywhere in matrix ops in neural networks. ✓
- Shapes:
Eis(0, 3),vis(3,)→ Rule 3 aligns it to(1, 3). Why this step? A size-0axis is still a legal size; Rule 3 (missing axis) and the later rules only care whether sizes clash, not whether they are empty. - Compare. Trailing:
3 vs 3→ Rule 1 (equal). Next:0 vs 1→ Rule 2, the1stretches to0. Why this step? Rule 2 says a singleton matches any size — and0counts as "any size". Stretching to0just means "make zero copies". - Result
(0, 3): a valid but empty array — no rows to fill. Why this step? The output takes the larger size on each axis;max(0,1)=0keeps the row axis empty, so there are simply no cells to compute.
Verify: result.shape == (0, 3) and result.size == 0. No ValueError fires. The key lesson: 0 is not the clash trigger — only two different, both-non-1 sizes clash. ✓
- Shapes: both rank 2, no padding:
(1, 2)and(1, 3). Why this step? Equal ranks, so Rule 3 stays quiet and we compare sizes directly from the right. - Trailing axis:
2 vs 3. Why this step? Neither is equal (2 ≠ 3, so Rule 1 fails) and neither is1(so Rule 2 cannot fire) — there is no singleton to stretch, so NumPy cannot invent a matching size. - Verdict:
ValueError: operands could not be broadcast together with shapes (1,2) (1,3). Why this step? When an axis pair passes none of the three rules, broadcasting is declared impossible and NumPy raises immediately.

The figure contrasts a rescue and a crash: (1,2) vs (2,1) (both have a 1, both stretch via Rule 2 → orange grid) beside (1,2) vs (1,3) (trailing 2 vs 3, both non-1 → magenta crash bolt).
Verify: The pairing that works — (1,2) vs (2,1) — gives (2,2); the pairing that fails — (1,2) vs (1,3) — has trailing 2 ≠ 3 with no 1. ✓
- Column means.
mu = [60, 70, 60](average down each subject column). Why this step?axis=0collapses the row axis, leaving one number per column — the per-subject average. - Shape of
mu:(3,)→ Rule 3 aligns it to(1, 3), sitting under the columns ofS. It then broadcasts down every student row. Why this step? We want the same subject-mean subtracted from every student — that is exactly a down-the-rows stretch (Rule 2 on the padded axis, as in Cell E), applied for real. - Column stds. For subject 0 the deviations are
[-10, 0, 10], so . For subject 2 the entries are[90, 30, 60]with mean60, so the deviations are[30, -30, 0]and — a much wider spread than subject 0. Why this step? Standardization divides by spread so every subject ends on the same scale; the spreads genuinely differ, so each column needs its own . - Divide.
(S - mu)is(3,3);sigmais(3,)→ Rule 3 makes it(1,3); Rule 2 broadcasts it down the rows again. ResultZis(3,3). Why this step? The same right-alignment that placedmuunder the columns also placessigmathere, so each column is divided by its matching std.
Verify: Every column of Z has mean by construction (we subtracted its mean) and population standard deviation (we divided by its std). Column-0 entries: , mean , std . Column-2 std check: , confirming the columns really do differ. ✓
- Shapes:
Xis(3, 100),weightsis(3,)→ Rule 3 aligns it to(1, 3). Why this step? Right-alignment forcesweights's3underX's trailing axis, which is100— not under the feature-axis3. - Trailing compare:
100 vs 3→ both non-1, different → Rule 1 and Rule 2 both fail → clash →ValueError(this is Cell G in disguise). Why this step? Human intuition matches by meaning ("3 weights = 3 features"); NumPy matches by position from the right. Meaning is irrelevant to the alignment rule. - The fix:
weights[:, np.newaxis]makes shape(3, 1). Why this step? Now the3sits underX's row-axis (3 vs 3, Rule 1) and the1sits under the sample-axis (1 vs 100, Rule 2 stretch). Both axes are now legal. - Result
good.shape == (3, 100): each feature-row scaled by its own weight. Why this step? With the1stretched to 100, weightw[i]sits across the whole of rowi, so every sample of featureiis multiplied by the samew[i].
Verify: bad raises ValueError; good.shape == (3, 100). Reshaping weights to (3,1) moves the 3 off the trailing slot and onto the row slot. ✓
- Shapes:
batchis(4, 2, 3)(rank 3),biasis(2, 3)(rank 2). Why this step? Ranks differ (3 vs 2), so Rule 3 must fire once — but only on the single leftmost axis thatbiaslacks. - Right-align. Rule 3 pads
biasto(1, 2, 3). Why this step? Padding goes on the left, sobias's(2, 3)lands underbatch's trailing two axes(2, 3), and a fresh size-1axis appears in the batch slot. - Compare all three axes, right to left. Trailing:
3 vs 3→ Rule 1. Middle:2 vs 2→ Rule 1. Leading:4 vs 1→ Rule 2, the padded1stretches to4. Why this step? The two trailing axes already match (Rule 1), and the padded batch axis is a singleton, so Rule 2 copies the single bias plane across all 4 images. - Result
(max(4,1), max(2,2), max(3,3)) = (4, 2, 3). Every one of the 4 images gets the same(2,3)bias plane added. Why this step? Each axis takes the larger size; because only the batch axis was a singleton, the bias plane is shared identically across the batch — exactly the "one bias per feature-map, reused per sample" pattern of neural-net matrix ops.
Verify: out.shape == (4, 2, 3), and since every image was all-ones, image k equals 1 + bias, e.g. out[0] and out[3] are both [[11,21,31],[41,51,61]]. Corner out[3,1,2] = 1 + 60 = 61. ✓
Active recall
Recall What is the
only condition that causes a broadcasting ValueError?
Two aligned axes have different sizes and neither is 1 — that is, both Rule 1 and Rule 2 fail. ::: Everything else — missing axes (Rule 3), singletons (Rule 2), even size-0 axes — broadcasts fine.
Recall What does "Rule 3 then Rule 2" actually mean?
Rule 3 only inserts a size-1 axis where an array has fewer dimensions; that new 1 is then an ordinary singleton, so Rule 2 is what stretches it. ::: Rule 3 never resolves an axis by itself — it always hands off to Rule 2.
Recall A
(3,) vector times a (3, 100) matrix crashes. Why, in one sentence?
Right-alignment puts the vector's 3 under the trailing axis 100 (not under the feature-axis 3), and 3 ≠ 100 with no singleton, so both Rule 1 and Rule 2 fail. ::: Fix with weights[:, np.newaxis] to make it (3, 1).
Recall When a
(4, 2, 3) batch meets a (2, 3) plane, which axis stretches and to what?
Rule 3 pads the plane to (1, 2, 3), then Rule 2 stretches that leading 1 to 4, so the one plane is reused across all 4 batch items. ::: Result stays (4, 2, 3).
For the speed payoff of never copying data, see 1.4.04-NumPy-vectorization, 1.4.06-NumPy-performance-optimization, and 1.2.04-Memory-efficient-Python-patterns; for the shape-manipulation tools like np.newaxis, revisit 1.4.03-NumPy-array-indexing-slicing.