Views vs Copies
In one line: basic slicing gives you a window onto the same memory; boolean and fancy indexing give you new memory — and the difference decides whether your writes are visible elsewhere.
The demonstration
a = np.arange(10)
view = a[2:5] # basic slicing → VIEW
view[0] = 999
a # array([0, 1, 999, 3, ...]) ← changed
copy = a[[2, 3, 4]] # fancy indexing → COPY
copy[0] = -1
a # unchanged
The table
| Operation | Result |
|---|---|
a[2:5], a[::2], a[:, 0] |
view |
a.T, a.reshape(...) (when compatible), a.ravel() |
view (usually) |
a0, 2, 4 |
copy |
a[a > 5] |
copy |
a.flatten() |
copy (always) |
a.astype(...) |
copy (always) |
a.copy() |
copy |
np.asarray(a) |
same object if already an array |
a + 0, any arithmetic |
new array |
np.concatenate, np.stack |
new array |
Note ravel() vs flatten(): ravel returns a view when it can, flatten always copies. If you want a guaranteed-independent 1-D version, use flatten. If you want speed and will not write to it, use ravel.
reshape returns a view when the requested shape is compatible with the existing strides. After a transpose it usually is not, so a.T.reshape(-1) silently copies. See Memory Layout and Strides.
How to check
b.base is a # True → b is a view of a
b.base # None → b owns its data
b.flags.owndata # False → it's a view
np.shares_memory(a, b) # definitive, can be slow
np.may_share_memory(a, b) # fast, conservative
b.base is a is the everyday tool. np.shares_memory is the one to reach for when debugging something genuinely confusing.
Why views exist
They are the reason NumPy scales.
big = np.zeros((100_000, 1000)) # 800 MB
window = big[50_000:50_100] # instant, 0 bytes
big.T # instant, 0 bytes
If slicing copied, every intermediate step in a pipeline over a large matrix would double your memory. Views make chained slicing free.
Where it hurts
1. The function that mutates its argument.
def normalise(matrix):
matrix -= matrix.mean(axis=1, keepdims=True) # IN-PLACE — mutates the caller's array!
return matrix
expr_norm = normalise(expr) # expr is now also normalised
The -= operates on the array the caller passed. Either document that clearly, or copy first:
def normalise(matrix):
matrix = matrix - matrix.mean(axis=1, keepdims=True) # new array
return matrix
x = x - y rebinds to a new array; x -= y mutates in place. In NumPy those are genuinely different operations, unlike for immutable Python types.
2. Keeping a small slice of a huge array alive.
big = np.zeros((100_000, 1000))
small = big[0:10]
del big
# the full 800 MB is STILL allocated — `small` holds a reference through .base
small = big[0:10].copy() # ← the fix
This is a real memory leak pattern when you slice a large file-loaded array and keep only the slice.
3. Chained assignment that goes nowhere.
a[a > 5][0] = 0 # a[a > 5] produces a COPY; the write hits a temporary and is lost
a[np.flatnonzero(a > 5)[0]] = 0 # correct
No error, no warning, no effect. This is the exact same failure mode as pandas chained assignment — see Copy-on-Write, which is why pandas 3.0 made it impossible.
Assignment is different from selection
a[a > 5] # __getitem__ → gather into new memory (copy)
a[a > 5] = 0 # __setitem__ → scatter into existing memory (in-place)
So masked reads copy but masked writes do modify the original. This asymmetry is correct but surprising the first time.
Defensive practice
- Copy at API boundaries. A function that takes an array from a caller and mutates it should say so in its name (
normalise_inplace) or copy. - Copy after slicing something large you want to keep.
roi = big[a:b].copy(). - Use
np.shares_memorywhen debugging "why did this change". - Prefer explicit
out=over relying on in-place operators when the intent matters. See ufuncs.
Bioinformatics example
# subsetting to a gene panel from a whole-transcriptome matrix
panel_idx = np.flatnonzero(np.isin(gene_names, PANEL))
panel_expr = expr[panel_idx] # fancy indexing → copy, safe
del expr # the big matrix is genuinely freed
# a chromosome window from a whole-genome coverage array
window = coverage[start:end] # view — do NOT modify unless you mean to
window_own = coverage[start:end].copy() # safe to modify
Common mistakes
- Mutating a view and corrupting the source.
- Expecting a mask-selected copy to write back.
a[mask][0] = xdoes nothing. - A hidden memory leak from a small view of a big array.
ravel()when you neededflatten().- Assuming
reshapenever copies. After a transpose it usually does. - Confusing
x = x + 1withx += 1on a view. The first is safe; the second writes through.
See also
Indexing and Slicing · Boolean Masking · Fancy Indexing · Memory Layout and Strides · Copy-on-Write · ndarray
Test cases · 2
| # | via | input | expected stdout |
|---|---|---|---|
| 1 | stdin | 1 2 3 4 | view_is_base=True original0=99 copy0=99 |
| 2 | stdin | 7 8 9 | view_is_base=True original0=99 copy0=99 |
Hints · 2
01Hint
Basic slicing returns a view that shares memory with the original.
02Hint
arr.base is not None when arr is a view of another array.