NumPy, from first principles
A NumPy array combines a data buffer with its element type, shape, and strides. Shape describes the dimensions; strides locate successive elements along each axis. We’ll use six numbers to see how these pieces work together.
A line of numbers, not a list
Section titled “A line of numbers, not a list”Why a flat, packed line at all? Because the obvious alternative — a Python list — doesn’t keep your numbers together. It keeps pointers to number-objects scattered across memory, each separately boxed with its own type tag. To add them up, the computer chases a pointer to a random address for every single value.
An array throws that away: same type, packed end to end, so element i is one fixed step away. Reading is a straight walk.
That packed line is the thing every idea below is built on.
Dimensionality is just how you walk the line
Section titled “Dimensionality is just how you walk the line”A “2-D array” isn’t laid out as a square in memory — it’s the same flat line, read in rows of a chosen width. Change the shape and the same twelve numbers become a row, a 3×4 grid, or a 2×2×3 cube. Nothing moves.
The number of dimensions is simply how many numbers are in the shape tuple. Strides are the other half of the instruction: how far to step in the flat line to move one along each axis. Together, shape and strides describe how to locate an element.
Where does element [i, j] actually live?
For a contiguous row-major (3, 4) array, moving down one row advances four elements; moving across a column advances one. Element [i, j] sits at flat position i*4 + j. NumPy stores strides in bytes. A transpose or basic slice shares the buffer; a reshape can share it when the layout permits, but may need a copy. See NumPy’s guide to copies and views.
Axes: name one, lose one
Section titled “Axes: name one, lose one”An axis is a direction to walk the array. In a reduction such as sum, mean, or argmax, the named axis is removed from the output shape by default. With keepdims=True, it remains as a dimension of length 1.
This is why axis=-1 turns up everywhere in deep learning: a batch of rows, collapse the last axis, get one answer per row. It’s the same argmax-over-the-final-axis you meet at the end of a transformer’s softmax.
Broadcasting: stretch, don’t copy
Section titled “Broadcasting: stretch, don’t copy”Two arrays with different shapes still combine — NumPy lines their shapes up from the right and stretches any length-1 axis to fit. The trick: it doesn’t copy the stretched values, it sets that axis’s stride to zero, so the same number in memory is simply read again and again.
That’s all broadcasting is: stretching is just re-reading.
Views: a slice shares the memory
Section titled “Views: a slice shares the memory”Slice an array and you don’t get new numbers — you get a view: a fresh (offset, shape, strides) pointing at the same line. So writing into the slice writes into the original. Ask for .copy() and you get a genuinely new line instead.
Vectorize: send the loop to C
Section titled “Vectorize: send the loop to C”Now the payoff. Because the line is packed and single-typed, you almost never write a Python loop over it. You write the whole-array expression a + b, and the loop runs inside NumPy, in compiled C, straight across that packed memory.
Reading an array operation
Section titled “Reading an array operation”For an unfamiliar operation, check the input shapes, the axis it acts on, and the output shape. If it produces another array, check whether that array shares memory with the original. These questions explain the behavior of the examples above.