Question bank — NumPy broadcasting rules
This is a thinking bank, not a calculator drill. Every item below targets a place where broadcasting feels one way but behaves another. Answer out loud first, then reveal. If you have never seen the notation, start with the parent note NumPy broadcasting rules — everything here assumes only these ideas from it:
- A shape is the tuple of sizes NumPy prints, e.g.
(2, 3)means 2 rows, 3 columns. - Throughout this vault
npis just the standard nickname for the NumPy library — every time you seenp.something, read it as "the NumPy tool called something". Sonp.newaxisis NumPy's marker meaning "insert a brand-new axis of size 1 here". - Trailing dimension = the rightmost number in the shape. Broadcasting compares shapes starting there.
- The three rules for each aligned pair: sizes are equal, OR one of them is 1 (singleton), OR one dimension is missing (padded with 1 on the left).
- A singleton dimension (size 1) can stretch to any size for free — no memory is copied.
Before the questions, look at the four figures below. They are the pictures behind the whole ritual — the words in every answer just re-tell what these show.
Figure 1 — the right-align + pad step. Two shapes are pushed against the right wall; the shorter one grows 1s on its left until the columns line up.

Look at the yellow 1s that appear on the left of the shorter shape — those are invented, not real data. Every question in this bank starts by drawing this picture in your head.
Figure 2 — the three rules, column by column. Once aligned, each column faces one of three verdicts: equal (blue, keep it), singleton stretch (pink, the 1 grows), or clash (a size vs a different size, neither being 1 → red ValueError).

The output size in each surviving column is simply the taller of the two boxes.
Figure 3 — what "stretch" actually does (no copies). A single row of size 1 is re-read to fill every output row. The pale arrows all point back to the same source memory — that is why broadcasting costs no extra RAM.

Notice: the output holds many genuine values, but the input was never duplicated in memory.
Figure 4 — the tricky edges. Three non-intuitive cases side by side: a 0-D scalar () padding to all-1s, an empty (0, 3) axis that still broadcasts, and the classic (3, 100) vs (3,) clash that looks like it should match on the 3 but does not.

Related prerequisite and follow-on notes: 1.4.03-NumPy-array-indexing-slicing, 1.4.04-NumPy-vectorization, 1.4.06-NumPy-performance-optimization, 2.1.02-Feature-normalization-standardization, 3.2.04-Matrix-operations-in-neural-networks, 1.2.04-Memory-efficient-Python-patterns.
True or false — justify
A (3,) array and a (3, 3) array always broadcast.
(3,) right-aligns as (1, 3) (Figure 1), so it broadcasts against the columns. That works for (3, 3) here, but if the (3, 3) were instead (3, 5) it would fail — the truth depends on the trailing dimension, not on the shared "3".Broadcasting physically copies the small array to fill the big shape.
A scalar has shape (1,).
() — zero axes (Figure 4, left). It behaves like all-1s only after the missing-axis padding rule turns it into effective (1, 1, …).If two shapes have the same number of dimensions, they are guaranteed to broadcast.
(2, 3) and (4, 3) have equal rank yet fail on the leading axis (2 vs 4, neither is 1).(1, 3) + (3, 1) gives a (3, 3) array.
3 vs 1 → stretch to 3. Row: 1 vs 3 → stretch to 3. Both axes stretch a different operand, producing every pairwise combination.Broadcasting compares dimensions left-to-right.
Adding a (3,) weight vector to a (3, 100) feature matrix scales each of the 3 features.
(3,) aligns as (1, 3) against the trailing 100 (Figure 4, right), so 3 vs 100 fails with a ValueError — the weights never touch the leading 3.The result of a successful broadcast has, in each axis, the maximum of the two aligned sizes.
max correctly describes the output size (treating a missing axis as 1).(0, 3) (an empty array) and (3,) broadcast fine.
3 vs 3 matches, leading 0 vs missing→1 stretches (Figure 4, middle); the result is a valid empty (0, 3) array. Zero-length axes obey the same rules as any other size.Spot the error
X is (3, 100), w is (3,); the code writes X * w to weight 3 features. What is wrong and how do you fix it?
w aligns as (1, 3), hitting the trailing 100 → ValueError. Fix: w[:, np.newaxis] makes it (3, 1), which stretches along the samples and matches the leading 3.A student "proves" (2, 3) + (3, 2) works because both contain a 2 and a 3.
3 vs 2 (neither equal nor 1) → immediate ValueError.Someone pads a scalar as (1,) before adding it to a (2, 3) matrix "to be safe."
() already pads to as many left 1s as needed. Forcing (1,) still only supplies one axis, which happens to also work here — but the mental model is wrong.To center columns, code does X - X.mean(axis=0, keepdims=False) for X of shape (4, 3). Any hidden trap?
keepdims=False the mean is (3,), which aligns to the trailing 3 — this one works. The trap appears with axis=1: X.mean(axis=1) is (4,), aligns to the 3, and fails; you need keepdims=True → (4, 1).a is (3,), b is (4,); the code does a * b expecting a (3, 4) outer product.
(3,) vs (4,) → 3 vs 4 fails. An outer product needs an explicit new axis: a[:, np.newaxis] * b → (3, 1) * (4,) → (3, 4).Code reshapes to (3, 1, 1) "because broadcasting needs matching rank."
Why questions
Why does broadcasting start from the trailing dimension instead of the leading one?
Why is a size-1 axis "free" to stretch but a size-2 axis is not?
Why does a missing dimension get padded with 1 on the left, not the right?
Why does NumPy raise ValueError instead of just repeating the shorter array to fit?
3 become 6? 9?). The strict rule guarantees a unique, predictable output and catches shape bugs early rather than producing silent nonsense.Why is broadcasting central to vectorization and neural-net math?
Why can (1, 3) and (2, 1) both stretch simultaneously?
1→2), the column axis stretches the second (1→3). Different operands supply the "1" in different axes, giving a full grid.Edge cases
Two 0-D scalars, shapes () and (), are added. What is the result shape?
() — the empty tuple. With no axes to compare, all rules pass vacuously and the output is itself a scalar.(5, 1) combined with (1, 5): does the result have any copied data?
(5, 5) and holds 25 genuine computed values, but the inputs were never expanded in memory — each was read with a stride-0 axis during the single pass (Figure 3).(2, 1, 4) and (3, 4): does it broadcast, and to what?
(2, 3, 4). Right-align: trailing 4 vs 4 (equal), middle 1 vs 3 (stretch), leading 2 vs missing→1 (stretch). Every axis passes.An array (0,) (empty) plus a scalar 5.
(0,). The operation is applied element-wise to zero elements — a valid, harmless no-op that returns an empty array, not an error.(4, 3) and (4,): does it broadcast?
(4,) aligns as (1, 4), so trailing 3 vs 4 fails. The shared 4 is on the wrong (leading) axis; you would need (4, 1) to match rows.A (1, 1) array added to a (1000, 1000) matrix — is this different from adding a plain scalar?
(1000, 1000)), but the (1, 1) array carries two explicit axes, so it also aligns like a 2-D array — handy when you later index or reduce it consistently.Recall The one-sentence test you should run in your head every time
Right-align the shapes, pad short ones with 1 on the left, and for every column ask "equal, or is one a 1?" — all yes ⇒ success with the per-axis max as the output size; any no ⇒ ValueError.