Views, Copies & Memory in NumPy
Understanding contiguous array memory layouts, stride tricks, array views, and explicit copies to avoid silent data mutation bugs.
Underlying Memory Architecture of NumPy
A NumPy array consists of two parts:
- Raw Data Buffer: A continuous block of memory bytes storing numbers.
- Array Metadata (Header): Shape tuple, Data type (dtype), Strides, and Memory Flags.
┌─────────────────────────────────────────────────────────────┐
│ ARRAY METADATA: Shape=(2,3), dtype=int64, Strides=(24,8) │
├─────────────────────────────────────────────────────────────┤
│ RAW MEMORY BUFFER: [ 10 | 20 | 30 | 40 | 50 | 60 ] │
└──────────────────────────┬──────────────────────────────────┘
│
▼
Shared Memory Buffer!
Array Views vs Array Copies
┌──────────────────────────┬──────────────────────────┐
│ ARRAY VIEW │ ARRAY COPY │
├──────────────────────────┼──────────────────────────┤
│ Shares SAME memory buffer│ Allocates BRAND NEW │
│ as parent array! │ independent memory buffer│
│ Created by basic slicing │ Created by arr.copy() or │
│ (arr[0:5]) or reshape(). │ fancy indexing. │
│ Modifying view MUTATES │ Modifying copy DOES NOT │
│ parent array! Fast, zero │ mutate parent array! │
│ extra memory! │ Takes extra RAM! │
└──────────────────────────┴──────────────────────────┘
The Silent Mutation Trap
import numpy as np
parent = np.array([10, 20, 30, 40, 50])
view_slice = parent[0:3] # Basic slicing creates a VIEW!
view_slice[0] = 999 # Mutate the view...
print(parent)
# Output: [999, 20, 30, 40, 50] <── PARENT ARRAY WAS MUTATED SILENTLY!
To isolate modifications safely, call .copy() explicitly:
safe_copy = parent[0:3].copy() # Independent memory buffer
Strides and Memory Contiguity
Strides define how many bytes to step in RAM memory to move to the next element along each dimension:
- C-Contiguous (Row-Major): Elements in the same row sit in adjacent memory addresses. (Default in C and NumPy).
- Fortran-Contiguous (Column-Major): Elements in the same column sit in adjacent memory addresses. (Default in Fortran and R).
Transposing an array (arr.T) swaps strides without moving data in memory. This makes the transposed array non-contiguous, which can slow down GPU operations!
Fix non-contiguous arrays using np.ascontiguousarray(arr).
Checking Array Memory Status
arr = np.ones((3, 3))
print(arr.flags.c_contiguous) # True
print(arr.base is None) # True (Owns its memory!)
transposed = arr.T
print(transposed.flags.c_contiguous) # False (Non-contiguous view!)
print(transposed.base) # Points to original arr memory!
Say this out loud
NumPy arrays separate metadata from raw memory buffers. Basic slicing and reshaping create array views sharing the parent memory buffer, meaning mutations affect parent arrays silently. Explicit
.copy()calls allocate independent memory. Non-contiguous arrays created by transpositions should be converted usingascontiguousarrayfor fast memory access.
Followups to expect
- What is Fancy Indexing in NumPy? Indexing arrays using integer arrays or boolean masks (
arr[arr > 5]), which always returns a new Copy rather than a View. - How to check memory size of a NumPy array? Calculate total memory using
arr.nbytes(orarr.size * arr.itemsize).
Check yourself
What happens to the original parent NumPy array when you modify elements in a basic slice View (e.g. sub = arr[0:5])?