NumPy Memory Views — Core Concepts
Why this topic matters
The view/copy distinction is the single most common source of confusion for intermediate NumPy users. Getting it wrong leads to data corruption (when you modify a view expecting a copy) or wasted memory (when you copy data unnecessarily). Understanding the underlying memory model turns these from mysterious bugs into predictable behavior.
Views vs copies — the rules
| Operation | Returns | Example |
|---|---|---|
| Basic slice | View | a[2:5], a[:, 0] |
| Transpose | View | a.T |
| Reshape (if contiguous) | View | a.reshape(3, 4) |
| Fancy indexing | Copy | a[[0, 2, 4]] |
| Boolean indexing | Copy | a[a > 0] |
.copy() | Copy | a.copy() |
| Arithmetic operations | New array | a + b |
How to check: view or copy?
import numpy as np
a = np.arange(12).reshape(3, 4)
b = a[1:3] # slice — should be a view
c = a[[0, 2]] # fancy index — should be a copy
print(b.base is a) # True — b is a view of a
print(c.base is a) # False — c is independent
print(np.shares_memory(a, b)) # True
print(np.shares_memory(a, c)) # False
The .base attribute points to the owning array. If it is None, the array owns its data. np.shares_memory() is the most reliable check.
How strides make views possible
Every NumPy array has:
- A data pointer — the address of the first element in memory.
- A shape — the size of each dimension.
- Strides — how many bytes to jump when moving one step along each dimension.
a = np.arange(12).reshape(3, 4)
print(a.strides) # (32, 8) — 32 bytes per row, 8 bytes per element
A view changes the shape and/or strides but keeps the same data pointer:
b = a[::2] # every other row
print(b.strides) # (64, 8) — doubled row stride, same column stride
print(b.base is a) # True — same data
Transposing swaps strides without moving data:
t = a.T
print(t.strides) # (8, 32) — swapped from (32, 8)
print(t.base is a) # True
The reshape caveat
reshape returns a view only if the new shape is compatible with the current memory layout. If the array is not contiguous (e.g., after transposing), reshape must copy:
a = np.arange(12).reshape(3, 4)
b = a.T
c = b.reshape(12) # Must copy — b is Fortran-contiguous, 12 is C-contiguous
print(c.base is a) # False — copy was made
To force a view (and get an error if impossible), use np.reshape with order or check .flags:
print(b.flags['C_CONTIGUOUS']) # False
print(b.flags['F_CONTIGUOUS']) # True
Common misconception
Many developers believe that assignment through a slice modifies the view object. It actually modifies the underlying data:
a = np.zeros(5)
b = a[1:4] # view
b[:] = 99 # modifies a through b
print(a) # [0, 99, 99, 99, 0]
This is intentional and powerful — it enables in-place updates without copying. But it means you must be deliberate about whether you want to modify the original.
Defensive patterns
# Pattern 1: Force a copy when you need independence
safe = a[1:4].copy()
# Pattern 2: Make arrays read-only to prevent accidental mutation
a.flags.writeable = False
# a[0] = 99 → ValueError
# Pattern 3: Check before modifying
if np.shares_memory(a, b):
b = b.copy()
The one thing to remember: Slices are views (shared data), fancy indexing creates copies (independent data) — check with np.shares_memory() when in doubt.
See Also
- Python Bokeh Get an intuitive feel for Bokeh so Python behavior stops feeling unpredictable.
- Python Numpy Advanced Indexing How to cherry-pick exactly the data you want from a NumPy array using lists, masks, and fancy tricks.
- Python Numpy Broadcasting Rules How NumPy magically makes different-sized arrays work together without you writing any loops.
- Python Numpy Einsum One tiny function that replaces dozens of NumPy operations — once you learn its shorthand, array math becomes a breeze.
- Python Numpy Fft Spectral How NumPy breaks apart a signal into its hidden frequencies — like separating a chord into individual notes.