Array Views vs Copies
Some NumPy operations return a new window onto the same memory, others allocate a fresh block — mixing them up is one of the most common sources of silent bugs in quant Python code.
Prerequisites: NumPy Broadcasting
A backtest slices out a window of a price array to compute a rolling signal, then modifies that slice to add a small adjustment — and the original price array changes too, even though nothing touched it directly. This is not a bug in NumPy; it's the difference between a view, which shares memory with its parent array, and a copy, which owns independent memory. Not knowing which operation returns which is one of the most common causes of quietly wrong backtests.
Basic slicing (arr[2:5], arr[:, 0]) returns a view — same underlying memory, a new way of looking at it. Fancy indexing (arr1,3,5), boolean masking (arr[arr > 0]), and most arithmetic (arr + 1) return copies — new, independent memory. Modify a view and you modify the original; modify a copy and the original is untouched.
Why this happens
NumPy arrays store their data in one contiguous memory block, plus metadata (shape, strides) describing how to read that block. A slice like arr[2:5] can be expressed entirely as new metadata pointing into the same block — no data needs to move, so NumPy returns a view for free. Fancy indexing with a list of arbitrary positions, or a boolean mask, generally can't be expressed as a simple reinterpretation of strides — the selected elements aren't a regular stride pattern — so NumPy has to allocate new memory and copy the selected values into it.
Worked example
prices = np.array([100.0, 101.0, 102.0, 103.0, 104.0])
window = prices[1:3] # view: [101.0, 102.0]
window[0] = 999.0 # mutate the view
print(prices) # [100.0, 999.0, 102.0, 103.0, 104.0] — parent changed!
mask_selected = prices[prices > 102.0] # copy: [999.0, 103.0, 104.0]
mask_selected[0] = -1.0
print(prices) # unchanged by the mutation above — mask_selected is independent
The slice prices[1:3] shares memory with prices, so writing to window[0] silently rewrote prices[1]. The boolean mask, by contrast, built a new array; mutating it has no effect on prices at all. Both are correct NumPy behavior — the danger is assuming one when the code actually does the other.
What this means in practice
This distinction matters most when a function receives an array, slices it, and hands the slice to code that mutates it in place — a common pattern in vectorized signal pipelines — because that mutation can leak back into the caller's original data unexpectedly. .copy() forces an explicit, independent array whenever isolation is needed, and .base (returns None for a true owner, or the parent array for a view) lets you check which kind of array you're holding. When performance matters, views are effectively free; when correctness matters, an explicit .copy() is cheap insurance.
Chained indexing like arr[mask][0] = 5 often silently fails to modify arr at all, because arr[mask] already produced an independent copy before the second [0] = assignment touches it — NumPy may or may not warn about this "chained assignment" depending on the exact pattern. Prefer a single combined index, arr[mask][idx] → arr[np.where(mask)[0][idx]]-style single-step indexing, when you need to write back to the original.
Practice in interviews
Further reading
- NumPy documentation, 'Copies and Views'
- Harris et al., 'Array Programming with NumPy' (Nature, 2020)