36 · NumPy arrays: dtype, shape, slicing
Leave Python lists behind — NumPy's ndarray is a typed, contiguous buffer that runs arithmetic in C. Master creation, shape manipulation, and the slicing patterns every ML pipeline uses.
A Python `list` of floats stores each number as a separate 28-byte CPython object plus an 8-byte pointer. A NumPy `ndarray` stores them **packed side-by-side in a single C buffer** — each 64-bit float takes exactly 8 bytes. That compact layout is why NumPy can add two million-element arrays thousands of times faster than a Python loop: the CPU's vector units can sweep through the buffer without object-unpacking overhead.
Without this:
Without NumPy, every numeric algorithm would loop over Python lists — hundreds of times slower for anything matrix-shaped. NumPy is the substrate on which pandas, scikit-learn, PyTorch, and TensorFlow are all built. Knowing ndarray semantics (dtype, shape, strides, views) lets you read framework source code, debug shape mismatches, and write efficient preprocessing.
NumPy (Numerical Python) exists because CPython objects are expensive. A bare Python float is a full object — header, reference count, type pointer, and then the actual 8 bytes of value. Stored in a list, you also pay a pointer per element to link the object. For a million numbers that is ~36 MB of bookkeeping overhead.
A NumPy ndarray sidesteps all of that. It allocates one contiguous block of memory and writes values end-to-end with no per-element metadata. For a million float64 values that is exactly 8 MB. The shape, dtype, and strides are stored once in a small header object.
Beyond memory, the contiguous layout lets NumPy dispatch to highly optimised BLAS/LAPACK routines and, crucially, releases the Python GIL for the duration of C-level loops. Multiple cores can crunch numbers in parallel. This is why the Python ML stack is fast despite Python's interpreter being single-threaded.
Python (in browser)
`np.arange(10)` mirrors Python's `range(10)` but returns an ndarray. `np.linspace(0, 1, 5)` is indispensable for ML: it generates evenly-spaced feature grids and learning-rate schedules. Note how `.shape` is always a tuple — even a 1D array reports `(4,)`, not `4`.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
The comma-inside-brackets syntax `A[1, 2]` is NumPy's multi-dimensional indexing — equivalent to `A[1][2]` but faster because it avoids creating an intermediate array. `A[:, 0]` (all rows, column 0) is the idiom for extracting an entire feature column from a dataset matrix.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
Boolean masking returns a new array containing only the elements where the mask is `True` — no Python loop needed. `np.maximum(0, x)` is the vectorised ReLU activation function used in virtually every neural network. Fancy indexing (`a[[0, 2, 4]]`) selects arbitrary rows/elements and is how mini-batch sampling is implemented in data loaders.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`reshape(-1, cols)` is in virtually every PyTorch model's `forward` method — when a batch of images `(B, C, H, W)` needs to become a flat feature vector for a dense layer you write `x.reshape(B, -1)` and NumPy (or PyTorch) works out the remaining dimension automatically.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
What does `a.reshape(-1, 2)` do when `a` is a 1D array with 10 elements?
MML lesson 2 introduces vectors as ordered tuples in Rⁿ — exactly what a NumPy 1D array represents. A shape `(n,)` array *is* a vector; a shape `(m, n)` array *is* a matrix. The dtype is the choice of field (float64 ≈ ℝ, complex128 ≈ ℂ). The same index notation `v[i]` and slice notation `v[i:j]` map directly to the mathematical subscript `vᵢ`.
- A NumPy `ndarray` is a typed, contiguous C buffer — each element occupies exactly `itemsize` bytes, with no per-element Python object overhead. This is why it is 100–1000× faster than a Python list for numeric work.
- Create arrays with `np.array`, `np.zeros`, `np.ones`, `np.arange`, or `np.linspace`. Inspect them with `.shape`, `.ndim`, `.dtype`, `.size`.
- Multi-dimensional indexing uses `A[row, col]`; slicing uses `A[r_start:r_end, c_start:c_end]`. Boolean masking (`a[a > 0]`) and fancy indexing (`a[[0, 2, 4]]`) select subsets without loops.
- `a.reshape(rows, cols)` reinterprets the buffer into a new shape. Use `-1` for one dimension to let NumPy infer it — the `reshape(-1, ...)` idiom appears in nearly every PyTorch `forward()` method.
Every numeric ML pipeline is a chain of ndarray transformations. A CSV of training data becomes a `(n_samples, n_features)` array; labels become a `(n_samples,)` array; a mini-batch is a `(batch_size, ...)` slice. PyTorch `Tensor` and TensorFlow `tf.Tensor` mirror the NumPy ndarray API — `.shape`, `.dtype`, `.reshape`, boolean indexing — so fluency here transfers directly.
If you remove it: Without understanding ndarrays, every shape mismatch error in PyTorch or scikit-learn is a mystery. The `RuntimeError: shape '[64, -1]' is invalid` message is only meaningful if you know what `.reshape` does and why -1 sometimes fails. Knowing ndarray fundamentals turns cryptic framework errors into immediately diagnosable shape arithmetic.