Quant Memo
Core

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.

arr[2:5] — view same memory block, new shape/stride metadata arr1,3 — copy newly allocated, independent block
Slicing reuses the parent's memory with new metadata; fancy indexing and masking must gather scattered elements into a fresh block.

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.

Related concepts

Practice in interviews

Further reading

  • NumPy documentation, 'Copies and Views'
  • Harris et al., 'Array Programming with NumPy' (Nature, 2020)
ShareTwitterLinkedIn