Strides and Memory Layout
How many bytes to skip to move one step along each axis of an array — the hidden detail behind why some NumPy operations are free and others silently trigger a slow copy.
Prerequisites: Array Views vs Copies
A NumPy array looks like a grid, but underneath it's just one long, flat run of bytes in memory plus a small set of numbers that say how to walk through them. Those numbers — the strides — are why arr.T (transpose) is instant regardless of array size, while some other reshaping operations quietly copy the whole array. Understanding strides turns "why is this fast" and "why did this just get slow" from mysteries into arithmetic.
A stride is the number of bytes to jump in the flat underlying memory to move one step along a given axis. Reshaping, transposing, and slicing can often be done by just changing the strides and shape metadata — no data moves. An operation only forces a real copy when the requested layout can't be described by any stride pattern.
How strides work
Consider a 2D array of float64 (8 bytes each) with shape (3, 4), stored in the default row-major ("C") order: row 0's four elements sit contiguously, then row 1's, then row 2's. To move one step along axis 1 (across a row), you skip 8 bytes — one element. To move one step along axis 0 (down a column), you skip 4 elements' worth, or 32 bytes. So the strides are (32, 8).
Transposing this array to shape (4, 3) doesn't move a single byte — NumPy just swaps the strides to (8, 32) and the shape to (4, 3). Reading "down a column" of the transposed array now means jumping 8 bytes, which is exactly what "across a row" meant before the transpose — because that's literally the same underlying data, reinterpreted.
Worked example
a = np.arange(12).reshape(3, 4) has strides (32, 8) in bytes (int64, 8 bytes each element, 4 elements per row). a.T has strides (8, 32) — free, instant, no copy, confirmed by a.T.base is a being True.
Now try a.T.reshape(2, 6). This cannot be expressed by adjusting strides alone, because the transposed array's elements, read in the new (2, 6) order, aren't a simple stride-walk over the original contiguous block — the data would have to be read in an order that skips around. NumPy detects this and silently falls back to copying the data into a new, contiguous block before reshaping. You can check a.T.reshape(2, 6).base is a.T — it will be False, since .reshape() had to allocate.
What this means in practice
Knowing this explains real performance differences: .T, basic slicing, and .reshape() on an already-contiguous array are essentially free; .reshape() after a transpose, or on a non-contiguous slice, silently costs a full copy. For large arrays — a multi-year tick history, a big covariance matrix — that silent copy can be the difference between a function call that returns instantly and one that stalls and doubles memory use. arr.flags['C_CONTIGUOUS'] and arr.strides are the two things to check when a pipeline is mysteriously slow or memory-hungry.
np.ascontiguousarray() and .copy(order='C') force a real copy into row-major layout — useful before handing an array to a library (e.g. some C extensions) that assumes contiguity, but calling it reflexively "just in case" on every array defeats the whole point of views and can silently double memory use in a large pipeline.
Practice in interviews
Further reading
- NumPy documentation, 'Internal Memory Layout of an ndarray'
- Harris et al., 'Array Programming with NumPy' (Nature, 2020)