Articles
NumPy Broadcasting Interview Questions: Shapes, Views, and Silent Bugs
Practice NumPy broadcasting interview questions with shapes, keepdims, views, copies, silent axis bugs, tested examples, and realistic memory calculations.
NumPy broadcasting interview questions test more than whether two shapes are compatible. A strong answer explains which axis each value belongs to, whether the result matches the intended mathematics, and which arrays share memory. Code can run without an exception and still subtract the wrong means from every sample.
The useful habit is predict the shape, predict one value, then verify ownership and allocation. This article adds a square-matrix bug, a view-versus-copy probe, and a pairwise-distance memory calculation.
The examples are original teaching fixtures executed locally with NumPy 2.5.3. Official NumPy v2.5 documentation was checked September 21, 2026. Memory sizes below are calculated from shapes and element sizes, not measured production benchmarks.
How do you determine the broadcast result shape?
Compare dimensions from the right. A pair is compatible when the lengths match or one length is one; absent leading dimensions behave like singleton dimensions. If any aligned pair is incompatible, the operation fails. The rule concerns array layout, not the business meaning of an axis. NumPy broadcasting guide
Write the axes next to the shape before applying that rule. For X with shape (n, d), suppose rows are samples and columns are features. A vector (d,) naturally aligns with the feature axis. A vector (n,) does not automatically mean “one value for each sample,” because NumPy still aligns it with the final axis.
| Shapes | Result | Meaning to verify |
|---|---|---|
(4, 3) and (3,) | (4, 3) | Per feature |
(4, 3) and (4, 1) | (4, 3) | Per sample |
(4, 1) and (1, 3) | (4, 3) | Outer operation |
(4, 3) and (4,) | Error | Wrong alignment |
The last row is useful because it fails loudly. The dangerous version appears when the sample count happens to equal the feature count. Then the wrong vector length is accepted and a shape assertion alone cannot expose the mistake.
Worked bug: why does row centering fail silently on a square matrix?
Row centering subtracts each row's own mean from every element in that row. For the matrix containing 1 through 9 in row order, the row means are 2, 5, and 8. Every centered row should therefore be [-1, 0, 1].
import numpy as np
X = np.array([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]])
row_means = X.mean(axis=1) # shape (3,)
wrong = X - row_means # accepted, wrong axis
right = X - X.mean(axis=1, keepdims=True)
np.testing.assert_allclose(
right, [[-1., 0., 1.], [-1., 0., 1.], [-1., 0., 1.]])
np.testing.assert_allclose(right.mean(axis=1), 0., atol=1e-12)
assert not np.allclose(wrong, right)
print(wrong)
The wrong result is [[-1, -3, -5], [2, 0, -2], [5, 3, 1]]. NumPy subtracts 2 from column zero, 5 from column one, and 8 from column two. It has no way to know those three numbers were intended to belong to rows.
keepdims=True leaves the reduced axis at length one, producing (3, 1). That preserves the alignment needed for this subtraction. The official mean API documents the retained dimensions; the numerical fixture proves why they matter here. NumPy mean reference
Now change the input to (4, 3). The buggy row mean has shape (4,), so subtraction raises a shape error. The corrected version still returns (4, 3). Test both square and non-square shapes: the square case detects silent semantics, while the non-square case detects incompatible alignment.
When should you add an axis instead of reshaping everything?
If weights.shape == (n,) and each weight belongs to one sample, weights[:, None] expresses the intended (n, 1) shape. If a bias belongs to features, (d,) or (1, d) can express that intent. The important point is the named axis, not a preference for one spelling.
Avoid flattening and reshaping until an operation happens to run. A reshape preserves element order under its specified order convention; it does not infer which samples, channels, or time steps belong together. A passing element count is weaker evidence than a correct axis mapping.
For a batch shaped (batch, time, channels), a (channels,) scale applies across channels. A per-batch scale should be (batch, 1, 1). A (batch,) vector can silently align with channels if those sizes happen to match. Use deliberately different test dimensions such as (2, 3, 5) to make accidental equality less forgiving.
Check masks with the same care. A validity mask (batch, time) usually needs [..., None] to combine elementwise with (batch, time, channels). Boolean indexing is a different operation from broadcasted multiplication or where; it can select a differently shaped result. State whether you want to preserve the tensor layout or extract a subset.
Does broadcasting copy the input array?
Broadcasting can reuse input values without allocating a physically repeated input. That does not mean the whole expression is free. An ordinary arithmetic result generally needs storage for its output, and chained expressions can create additional temporaries. Separate the small input view from the potentially large computed result.
np.broadcast_to makes this distinction easy to probe. Its documented result is a read-only view; multiple apparent elements may refer to the same underlying location. Do not make it writable as a shortcut to independent storage. NumPy broadcast_to reference
base = np.array([10., 20., 30.])
expanded = np.broadcast_to(base, (4, 3))
assert np.shares_memory(base, expanded)
assert not expanded.flags.writeable
base[0] = 99.
assert np.all(expanded[:, 0] == 99.)
independent = expanded.copy()
assert independent.flags.writeable
assert not np.shares_memory(base, independent)
All four apparent copies of the first value change when the base changes. After .copy(), the new array has independent storage. If an interviewer asks whether this is “zero-copy,” qualify the statement: the expanded view shares the input, but copying it or computing a full arithmetic output allocates memory.
Inspect shape, strides, dtype, and flags when debugging. The logical nbytes of a broadcast view describes its element count times item size; it should not be mistaken for uniquely owned backing memory. That distinction matters when estimating how much memory a transformation actually adds.
How do slices, advanced indexing, and reshape affect views?
A basic slice normally shares the source buffer, while advanced indexing produces a copy when retrieving values. NumPy's copies-and-views guide documents this difference and explains that reshape can return a view or require a copy depending on layout. Do not promise that every reshape is free. NumPy copies and views
A = np.arange(12., dtype=float).reshape(4, 3)
slice_view = A[:, :2]
indexed_copy = A[:, [0, 1]]
assert np.shares_memory(A, slice_view)
assert not np.shares_memory(A, indexed_copy)
slice_view[0, 0] = -7.
assert A[0, 0] == -7.
assert indexed_copy[0, 0] == 0.
The two selections initially have equal values and the same shape, but different mutation behavior. This is why checking only printed output is insufficient. If a helper must not modify its caller's array, decide whether it should copy, produce a new result, or document mutation as part of its contract.
Advanced-index assignment is a separate case: A[:, [0, 1]] = 0 assigns into A. Do not infer from the copied retrieval above that assignment to that selection cannot change the original. In-place arithmetic also needs compatible output shape and dtype; a small left operand cannot simply grow to the broadcasted result.
For these small tests, np.shares_memory directly checks overlap. Its documentation warns that exact overlap checking can be expensive for difficult strided inputs; it is a diagnostic, not a free check to insert indiscriminately into a hot loop. NumPy shares_memory reference
Why can a short pairwise-distance expression exhaust memory?
Let X contain 2,000 vectors and Y contain 3,000 vectors, each with 64 float64 features. X[:, None, :] - Y[None, :, :] has shape (2000, 3000, 64). The expanded inputs may be views, but the subtraction result contains 384 million values.
At eight bytes per value, that single intermediate needs 3,072,000,000 bytes, or 3.072 decimal GB. Squaring it can require another temporary, depending on how the expression is written. The final (2000, 3000) distance matrix alone needs 48,000,000 bytes. These are shape calculations, not measured peak resident-memory figures.
For squared Euclidean distance, an algebraic alternative uses row squared norms plus column squared norms minus twice the matrix product. That avoids the three-dimensional difference tensor, but still creates a two-dimensional output and can suffer cancellation for nearby large vectors. Small negative roundoff values may need careful treatment; blindly clipping a large negative result can hide a real bug.
If the task only needs each X row's nearest neighbor, process Y in blocks and retain the best distance and index per X row. You need not store the full output matrix. Specify tie-breaking so block order cannot change which equally close neighbor wins. A lower-memory design can matter more than replacing every Python loop with a single expression.
The broader machine learning coding interview guide covers model implementation. Here, the key interview signal is naming the output and temporary shapes before allocating them.
Which tests catch broadcasting bugs that shape checks miss?
Use a tiny loop-based reference for correctness, even when the final implementation must be vectorized. For row centering, subtract each row's scalar mean in the reference and compare values. Keep that reference limited to small fixtures so it remains simple and independent of the same broadcasting mistake.
Test (2, 3), (3, 3), (1, 3), and (3, 1), plus an explicitly chosen empty-input policy. A singleton dimension can hide an error as easily as a square matrix. Avoid unrestricted squeeze() when a one-sample batch still needs a batch axis.
Add invariants tied to the mathematics: row-centered means should be approximately zero; row-wise softmax probabilities should sum to one along the class axis; distances should have the expected symmetry when comparing a matrix with itself. Use tolerances appropriate to dtype and magnitude rather than asserting exact floating-point equality everywhere.
Finally, check mutation and memory separately. Preserve a copy of the input when testing a non-mutating contract, verify relevant views with a small sharing probe, and calculate the largest intermediate shape. Passing values, shapes, and aliasing tests provides stronger evidence than the absence of a ValueError.
Practice the shape, value, and memory explanation
Attention and calibration extend the reasoning into model code; they are not all NumPy-only tasks or predictions of a specific interview.
| Practice question | Practice focus |
|---|---|
| Implement vectorized NumPy ops and explain broadcasting | Axis contracts |
| Implement and Debug Backprop in NumPy | Gradient shapes |
| Compute entropy and implement 1-NN | Pairwise memory |
| Implement and analyze custom attention | Mask axes |
| Derive and implement calibration via temperature scaling | Sum axes |
Attempt the vectorized NumPy exercise by writing each intermediate shape beside the expression. Then predict one output value and one memory-sharing relationship. That turns “broadcasting works” into an explanation an interviewer can actually verify.