5.4.4 · D5Scientific Computing (Python)

Question bank — Broadcasting — rules, why it works, gotchas

1,476 words7 min readBack to topic

True or false — justify

True or false: (3,4) + (4,) and (3,4) + (3,) both work.
False. Only (4,) works. Right-alignment pairs the length-(4,) axis with the trailing axis (size 4) → equal ✓. The length-(3,) axis pairs with size 4 → mismatch, neither is 1 → error.
True or false: broadcasting always makes the result at least as big as the larger input.
True. Each result dimension is the max of the pair, and missing leading dims count as 1. So no output axis is ever smaller than the biggest corresponding input axis.
True or false: adding a scalar to an array is a special code path, separate from broadcasting.
False. A scalar has shape (); missing dims become 1, then stretch. A + 5 is just the simplest broadcast — same rules, no special case.
True or false: if two shapes have the same number of elements, they can be broadcast together.
False. Element count is irrelevant; only the per-axis, right-aligned rule matters. (6,) and (2,3) have 6 elements each but (6,)(1,6) vs (2,3): pairs (1 vs 3)→3, (6 vs 2)→error.
True or false: (3,) + (3,1) gives a shape (3,) result.
False. It gives (3,3). (3,)(1,3), and (3,1) stays; pairs become (3 vs 1)→3 and (1 vs 3)→3. You accidentally built a matrix.
True or false: broadcasting a (1000,1) array against a (1,1000) array duplicates a million values in memory for the inputs.
False. Neither input is copied — the size-1 axes get stride 0. Only the (1000,1000) output is allocated; the operands are stride-0 views.
True or false: np.broadcast_to(a, shape) returns an array you can assign into like any other.
False. It is read-only: many output indices map to the same memory cell, so a write would be ambiguous. You must .copy() first.
True or false: since a size-1 axis "stretches to match," a size-2 axis can stretch to size 4.
False. Only 1 is the wildcard. Size 2 vs size 4 has no consistent way to pair element i with element i, so it's an error. Stretching is exclusive to 1.
True or false: the order of operands changes whether broadcasting succeeds.
False (for compatibility). Compatibility and result shape are symmetric in the two shapes. Only the values differ for non-commutative ops like subtraction/division; the shape rules do not.

Spot the error

M is (3,4), w is (3,), and you write M * w to scale each row. What breaks?
w→ trailing axis (size 4) by right-alignment; 3 vs 4 mismatch → error. Rows live on axis 0, so you need w[:, None](3,1) to target that axis.
You want to scale each column of M (shape (3,4)) by v of shape (4,) and write M * v[:, None]. Fix it.
v[:, None] makes (4,1), which right-aligns as (4 vs 4)? No — pairs are (4 vs 3) on axis 0 → error. Columns are the trailing axis, so use bare v (shape (4,)) or v[None, :](1,4).
a = np.arange(3), b = np.arange(3), and you expect a[:, None] + b to be shape (3,). Where's the bug?
a[:,None] is (3,1), b is (3,)(1,3); result is (3,3), an outer sum table, not a length-3 vector. If you wanted element-wise, use a + b.
You do X - X.mean(axis=1) to subtract each row's mean from X (shape (100,3)). Why is this wrong?
X.mean(axis=1) is (100,), which right-aligns with the trailing axis (size 3) → 100 vs 3 mismatch → error. Use X - X.mean(axis=1, keepdims=True)(100,1) to align with the rows axis.
np.broadcast_to(a, (3,4))[0,0] = 9 runs without a shape error in your head but crashes at runtime. Why?
The broadcast view is read-only; assigning is forbidden because that single cell is shared by many logical positions. .copy() the view before writing.
You reshape a (6,) array to (2,3) expecting it to broadcast against another (6,). What's the trap?
After reshape they no longer share a compatible layout: (2,3) vs (6,)(1,6) pairs (2 vs 1)→2, (3 vs 6)→error. Reshaping changes the broadcast picture entirely.

Why questions

Why does broadcasting align shapes from the right, not the left?
The trailing axes are the "innermost" fastest-varying dimensions and usually carry the meaningful feature axis (e.g. columns/features). Right-alignment matches a (features,) vector to the feature axis of a (samples, features) matrix automatically.
Why is a size-1 axis a valid stretch but size 0 or size 2 is not?
Size 1 means "one value, the same for everyone," so reusing it across any length is meaning-preserving. Size 2 has two distinct values with no rule to spread over n≠2 slots; size 0 has none at all.
Why does broadcasting cost no extra input memory?
The stretched axis gets stride 0 — "move to the next element" jumps 0 bytes and re-reads the same cell. It's a pointer trick, so no operand data is duplicated; only the output buffer is allocated.
Why is w[:, None] preferred over np.tile(w, (1,4)) to scale rows?
w[:, None] creates a stride-0 view ((3,1)) — no copy, C-speed. np.tile physically replicates into a full (3,4) buffer, wasting memory and time for the same numerical result. See np.tile and np.repeat (explicit replication).
Why does keepdims=True in a reduction make later broadcasting easier?
It keeps the reduced axis as size 1 instead of removing it, so the result stays right-aligned with the original array (e.g. (100,1) vs (100,3)), and the size-1 axis stretches cleanly back. See Reductions and the axis argument.
Why can a scalar 5 add to any array shape without ever erroring?
Its shape () supplies all-1 dimensions after padding, and 1 is the universal wildcard that matches every size — so it broadcasts against anything.

Edge cases

Shapes (5,1,4) and (3,4) — result?
(5,3,4). Pad (3,4)(1,3,4); pairs (5 vs 1)→5, (1 vs 3)→3, (4 vs 4)→4.
Shapes (2,3) and (2,) — result?
ERROR. (2,) pairs with the trailing axis (size 3); 2 vs 3 mismatch, neither is 1.
Shapes (8,1,6,1) and (7,1,5) — result?
(8,7,6,5). Pad the second to (1,7,1,5); pairs (8 vs 1)→8, (1 vs 7)→7, (6 vs 1)→6, (1 vs 5)→5.
An empty axis: shapes (0,3) and (3,) — result?
(0,3). Pairs (0 vs 1)? No — (3,)(1,3), so (0 vs 1)→0 and (3 vs 3)→3. A size-0 axis is legal and broadcasts against 1; the output just has zero rows.
Two size-1 axes meet: shapes (3,1) and (1,4) — result and why "both stretch"?
(3,4). Pairs (3 vs 1)→3 and (1 vs 4)→4; each array stretches along its own 1-axis, producing a full grid where out[i,j] mixes row i and column j.
A 0-D array against a big one: shape () and (2,2) — result?
(2,2). () pads to (1,1), both axes stretch. This is exactly why scalars work.
Same shape on both sides, e.g. (3,4) and (3,4) — is broadcasting even happening?
Technically yes, but every pair is (equal, equal), so no axis stretches and no stride is set to 0 — it's plain element-wise arithmetic, the degenerate case of broadcasting.

Recall One-line survival kit

Right-align the shapes; every column must be equal or contain a 1; result is the max; missing left dims are 1; the stretched axis is free (stride 0), so trust it — but check .shape whenever a "vector" meets a "matrix." ::: This single sentence resolves every trap above.

Connections

  • Broadcasting — rules, why it works, gotchas
  • NumPy Arrays — shape, dtype, strides
  • Reshaping and newaxis (None) indexing
  • Reductions and the axis argument
  • np.tile and np.repeat (explicit replication)
  • Vectorization vs Python loops