NumPy in 8 Slides
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This third post is the engine room. The first two posts told you what an array is and why it is fast; this one shows the mechanism that makes the speed possible. The single most useful realization is that an ndarray is not really a grid at all — it is a flat block of bytes plus a tiny set of metadata that says how to read it.
Once you hold that model, a long list of NumPy behaviors that seem like separate rules — cheap reshapes, surprising shared mutations, broadcasting — turn out to be consequences of one design. This is the post that converts magic into mechanics.
The flat-buffer model is the key insight. Physically, an ndarray is one contiguous 1D run of bytes. Logically, a small header turns that run into an n-dimensional array by recording three things: the dtype (how to interpret each chunk of bytes), the shape (the logical dimensions), and the strides (how far to step for each axis).
Nothing about the multi-dimensional structure is stored as nested data. It is all computed on the fly from the header. That is why so many operations are nearly free — they change the header, not the bytes.
Strides are the mechanism that ties shape to the flat buffer. A stride is the number of bytes you advance to move one step along a given axis. For a 3-by-4 array of int64 values, each element is 8 bytes, so moving one column over is a stride of 8, and moving one row down — past four columns — is a stride of 32.
Indexing is therefore pure arithmetic: to find element [i, j], NumPy computes an offset from i and j and the strides, then reads that location. There is no traversal or search, just a multiply-and-add. This is why random access into an array is instant regardless of its size.
This flow diagram shows how one flat buffer becomes a logical 2D grid. The bytes [1,2,3,4,5,6] never move. Attaching shape (2,3) and strides (24,8) tells NumPy to read them as two rows of three. Change the shape and strides and the very same bytes become a different array.
The lesson is that shape and strides are an interpretation layered over fixed data. This is the foundation for the next slide: reshaping is just swapping that interpretation, which is why it costs nothing.
This snippet demonstrates the payoff of the strides model. reshape does not copy the six values — it returns a new view with different shape and strides pointing at the same buffer. The proof is in the last lines: writing b[0,0] = 99 also changes a[0], because both names refer to the same underlying memory.
This is enormously efficient — reshaping a billion-element array is instant — but it is also the source of one of NumPy's most common surprises, which the next slide addresses head-on.
The view-versus-copy distinction is where the strides model bites back. Reshaping, basic slicing, and transposing typically return views: new headers over shared memory. That is fast, but mutating a view silently mutates the original, which causes bugs that surface far from their cause. When you need data you can change independently, call .copy().
There is one reliable exception: fancy indexing — indexing with an integer array or a boolean mask — always returns a copy, because the selected elements generally are not a simple strided slice of the original. Knowing which operations view and which copy saves hours of debugging.
Broadcasting is how NumPy combines arrays of different but compatible shapes without writing loops or copying data. The rule is mechanical: line up the shapes from the right, and for each axis the sizes must either match or one of them must be 1, in which case the size-1 axis is virtually stretched to match the other.
The word 'virtually' is important. NumPy does not physically tile the smaller array; it uses a stride of 0 along the broadcast axis so the same data is reread. You get the convenience of an expanded array with none of the memory cost.
This snippet shows broadcasting producing a 2D result from a column and a row. The column has shape (3,1) and the row has shape (4,) — treated as (1,4). Aligned from the right, the axes are (3,1) and (1,4); each has a 1 that stretches, yielding a (3,4) grid where every combination is summed.
This pattern — combining a column vector and a row vector into a full matrix — is the canonical broadcasting example, and it appears constantly in real code: applying per-row offsets, building distance matrices, computing outer-product-style grids.
This decision tree encodes the broadcasting rule exactly. Walking two aligned axes from the right: if their sizes are equal, use them directly; otherwise, if one is 1, stretch it to match; if neither is 1 and they differ, NumPy raises a shapes-not-aligned error.
Memorizing this tiny tree is worth more than memorizing examples. Whenever a broadcast fails or behaves unexpectedly, mentally walk the axes from the right through these three branches and the cause becomes obvious.
ufuncs — universal functions — are where vectorized speed physically lives. A ufunc like np.add, np.multiply, or np.sqrt is a compiled C loop that applies one operation element-wise across an array while handling dtype coercion and broadcasting automatically. The familiar operators + - * / are just syntactic sugar that dispatch to these ufuncs.
Understanding that operators are ufuncs explains a lot: why a + b broadcasts, why mixing int and float promotes to float, and why these operations are fast. When you write vectorized code, you are really composing ufunc calls.
Memory order is the last piece of the engine. By default NumPy uses C order, storing data row by row, so elements within a row are adjacent in memory. Traversing along the last axis is therefore cache-friendly and fast, while jumping across the first axis means large strides and more cache misses.
For most code this is invisible, but for very large arrays or tight numerical loops the traversal order can meaningfully affect performance even when the results are identical. Knowing that order exists — and that you can request column-major order or transpose deliberately — is the difference between code that is merely correct and code that is also fast.
That is the machinery: a flat buffer plus shape, dtype, and strides; views that share memory; broadcasting that aligns shapes from the right; ufuncs that run the element-wise loops in C; and memory order that shapes performance.
With the model in hand, the next post gets hands-on. It is a code-heavy tour of the everyday patterns — creating arrays, slicing, masking, aggregating along axes, and broadcasting — that turn this understanding into working fluency.