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

OperationReturnsExample
Basic sliceViewa[2:5], a[:, 0]
TransposeViewa.T
Reshape (if contiguous)Viewa.reshape(3, 4)
Fancy indexingCopya[[0, 2, 4]]
Boolean indexingCopya[a > 0]
.copy()Copya.copy()
Arithmetic operationsNew arraya + 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.

pythonnumpydata-science

See Also