1.4.4 · D5Python & Scientific Computing
Question bank — NumPy arrays and vectorized operations
True or false — justify
TRUE or FALSE: A NumPy ndarray can hold a mix of integers and strings just like a Python list.
False. An ndarray is homogeneous — one dtype for all elements. If you feed mixed data, NumPy silently upcasts everything to a common type (often
<U... strings or object), quietly killing the speed you came for.TRUE or FALSE: a * b on two 2-D arrays performs matrix multiplication.
False.
* is the Hadamard (element-wise) product: . Matrix multiplication is @. Confusing them is the single most common NumPy bug.TRUE or FALSE: a += 1 and a = a + 1 do exactly the same thing.
False in effect on memory.
a += 1 writes into the existing strip (in-place), so any view of a also changes. a = a + 1 builds a new strip and rebinds the name, leaving old views untouched.TRUE or FALSE: Slicing A[1:3, :] gives you an independent copy you can safely scribble on.
False. A basic slice is a view — it shares memory with
A. Writing A[1:3, :] = 0 also zeros those rows in the original array.TRUE or FALSE: Boolean indexing data[data > 0] returns a view.
False. Fancy/boolean indexing returns a copy (the selected elements aren't contiguous, so NumPy can't describe them as a simple view). Modifying the result does not touch
data.TRUE or FALSE: np.zeros((3,4)) and np.zeros(3,4) do the same thing.
False. The shape must be one tuple argument:
np.zeros((3,4)). Passing np.zeros(3, 4) treats 4 as the dtype argument and raises an error.TRUE or FALSE: Vectorized code is faster only because C runs faster than Python.
Half true, so effectively false. The bigger win is avoiding per-element Python interpreter overhead (type checks, reference counting) and enabling SIMD ("Same Instruction, Multiple Data") plus cache-friendly contiguous access. Raw C speed alone isn't the whole story.
TRUE or FALSE: np.arange(0, 1, 0.1) always contains exactly 10 elements.
False — trust it and you'll get bitten. Float steps accumulate rounding error, so the count can wobble and the endpoint behaviour is fragile. Use
np.linspace(0, 1, 10) when the number of points must be exact.Spot the error
np.arange(0, 10, 2) — a student says this returns [0,2,4,6,8,10]. What's wrong?
The stop value
10 is exclusive, just like Python's range. It stops before 10, giving [0,2,4,6,8].A student writes x = np.linalg.inv(A) @ b to solve . Why is this discouraged?
Computing the inverse explicitly is slower ( with a bigger constant) and numerically less stable. Prefer
np.linalg.solve(A, b), which uses LU decomposition + substitution directly.result = [] then for x in data: result = np.append(result, f(x)) — why is this a performance trap?
np.append copies the whole array every iteration, giving total cost. Preallocate with np.empty(n) and fill by index, or just vectorize result = f(data).Someone tries to add arrays of shape (3, 4) and (5,) and is shocked it fails. Why?
Broadcasting aligns dimensions from the right: last dims
4 vs 5 are neither equal nor 1, so the shapes are incompatible and NumPy raises a ValueError.A student expects np.array([1, 2, 3]) / np.array([1, 0, 3]) to crash on divide-by-zero. What actually happens?
NumPy doesn't crash — it returns
inf (with a RuntimeWarning) for the middle element. Element-wise ufuncs handle bad values by producing inf/nan, not exceptions.b = a then modifying b; the student is confused that a changed too. What did they misunderstand?
b = a copies the name, not the data — both point at the same ndarray. To get an independent array use b = a.copy().np.ones(2, 5) throws an error but the student swears the syntax is fine. Fix it.
The shape must be a tuple:
np.ones((2, 5)). The second positional argument to np.ones is dtype, not a second dimension.Why questions
WHY does @ dispatch to BLAS while * does not?
@ is unambiguous matrix multiplication, a well-studied operation with hand-tuned BLAS routines (tiling, threading). * is element-wise and trivially parallel, so it uses NumPy's own ufunc machinery — no BLAS needed.WHY does broadcasting a small array over a big one not blow up memory?
NumPy never physically copies the small array; it uses stride tricks, setting the stride along the stretched axis to 0 so the same memory is re-read. "Big array Repeats Over Axis, Don't Copy Actual Storage."
WHY does NumPy store data in one contiguous block instead of pointers like a Python list?
Contiguity lets the CPU stream neighbouring values into cache and apply one SIMD instruction to many at once. A list of pointers scatters objects across memory, forcing slow random access.
WHY prefer np.empty(shape) then fill, over building results inside a loop?
np.empty allocates the memory once up front. Growing an array in a loop reallocates and copies repeatedly, turning linear work into quadratic.WHY is the naive angle formula's cousin — trusting float arange for a fixed count — a bad idea?
Because floating-point step sizes accumulate rounding, the last point may fall just inside or outside the range, changing the element count.
linspace fixes both endpoints and the count exactly.Edge cases
What shape results from broadcasting (4, 1, 3) with (5, 3)?
(4, 5, 3). Right-align to (4,1,3) and (1,5,3), then each size-1 axis stretches to its partner: 4, then 5, then 3.What does np.array([]) have for shape and dtype, and does math on it error?
Shape
(0,), dtype defaults to float64. Ufuncs on an empty array succeed and return an empty array — the loop simply runs zero times.What happens when you broadcast a scalar like a + 5 — what "shape" does 5 take?
A scalar broadcasts against any shape: it's treated as size-1 along every axis, so
a + 5 adds 5 to every element regardless of a's dimensions.If A is (3,3) and v is (3,), what does A + v do — row-wise or column-wise?
Right-alignment matches
v's length to A's last axis (columns), so v is added to every row. To add per-row instead, reshape to v[:, None] (shape (3,1)).What does A @ B require of the shapes, and what breaks at the boundary?
The inner dimensions must match:
(m,n) @ (n,p) → (m,p). If the middle n values differ, it's a ValueError — unlike element-wise ops, @ never broadcasts the shared dimension.Recall One-line self-test
If someone hands you A[1:3] and asks "safe to overwrite?", what's the reflex answer? ::: No — it's a view sharing memory; overwrite it and you overwrite A. Call .copy() first if you need independence.
Connections
- NumPy arrays and vectorized operations — the parent topic these traps stress-test.
- 1.4.01-Python-fundamentals — the list-vs-array, name-vs-object confusions start here.
- 1.4.02-List-comprehensions-and-generators — the loop patterns vectorization replaces.
- 1.4.05-Pandas-DataFrames — same view/copy traps resurface (the infamous
SettingWithCopyWarning). - 2.1.03-Matrix-operations-for-ML — the
*vs@distinction matters most here. - 2.2.04-Batch-gradient-descent — broadcasting bugs silently corrupt gradients.
- 3.1.02-Tensor-operations-in-PyTorch — identical broadcasting and view rules carry over to tensors.