ndarray
In one line: a contiguous memory buffer plus a shape, a dtype and strides — everything else in NumPy is consequences of that.
The anatomy
a = np.arange(12).reshape(3, 4)
a.shape # (3, 4) tuple of dimension lengths
a.ndim # 2 len(shape)
a.size # 12 product of shape
a.dtype # int64 element type — see [[NumPy dtypes]]
a.itemsize # 8 bytes per element
a.nbytes # 96 size * itemsize
a.strides # (32, 8) bytes to step per axis — see [[Memory Layout and Strides]]
a.base # None if it owns its data, else the array it views
a.flags # C_CONTIGUOUS, F_CONTIGUOUS, OWNDATA, WRITEABLE
a.base is the debugging tool people forget. If it is not None, you are holding a view and writes will propagate. See Views vs Copies.
The four constraints
These define what an ndarray is, and each one has a consequence you will feel:
- Homogeneous. Every element is the same dtype. → No mixed-type columns; that is what Pandas is for.
- Fixed size. The buffer is allocated once. →
np.appendreallocates and copies the whole array; never append in a loop. Build a list, then convert once, or preallocate withnp.empty. - N-dimensional and rectangular. No ragged rows. → Sequences of different lengths must be padded, or held in a list.
- Contiguous (usually). → Slicing is free; arbitrary reordering is not.
Shape is not a suggestion
Most NumPy errors are shape errors. Three shapes that look the same and are not:
np.array([1, 2, 3]).shape # (3,) 1-D, no orientation
np.array([[1, 2, 3]]).shape # (1, 3) row vector
np.array([[1], [2], [3]]).shape # (3, 1) column vector
A (3,) array is neither a row nor a column. When you need one explicitly:
v[None, :] # (1, 3) also v[np.newaxis, :]
v[:, None] # (3, 1)
v.reshape(-1, 1)
The [:, None] idiom is the workhorse of Broadcasting — it is how you subtract a per-row value from every row of a matrix.
Creating one
Covered fully in Array Creation, but the essentials:
np.array([[1, 2], [3, 4]])
np.zeros((3, 4)); np.ones((3, 4)); np.full((3, 4), np.nan)
np.arange(0, 10, 2); np.linspace(0, 1, 11)
np.empty((3, 4)) # uninitialised — fast but contains garbage
np.zeros_like(a) # match shape and dtype of a
Bioinformatics framing
A (20000, 500) float32 expression matrix is 40 MB — trivially fast for whole-matrix operations, and catastrophically slow if you loop over its 10 million elements in Python.
counts = np.loadtxt("counts.tsv", skiprows=1, usecols=range(1, 501))
counts.shape # (20000, 500) — genes × samples
cpm = counts / counts.sum(axis=0) * 1e6 # normalise per sample
logcpm = np.log2(cpm + 1)
gene_mean = logcpm.mean(axis=1) # per gene → shape (20000,)
Note axis=0 sums down columns (one total per sample) while axis=1 averages across rows (one value per gene). Getting this backwards silently produces plausible-looking nonsense. See Axes and Reductions.
Common mistakes
- Assuming
(n,)and(n,1)are interchangeable. They broadcast differently and produce silently wrong shapes. - Growing an array in a loop with
np.appendornp.concatenate— quadratic time. Collect in a list, convert once. - Ignoring integer overflow.
np.array([127], dtype=np.int8) + 1gives-128, no warning. Read counts belong inint32orint64. - Using
np.emptyand forgetting to fill it. The garbage looks like data. arr.reshape(...)without assigning. Reshape returns a new array; it does not mutate.arr.resize()does mutate, which is why it is rarely what you want.- Comparing floats with
==. Usenp.isclose.
See also
NumPy dtypes · Array Creation · Indexing and Slicing · Broadcasting · Views vs Copies · Memory Layout and Strides · NumPy
Test cases · 2
| # | via | input | expected stdout |
|---|---|---|---|
| 1 | stdin | 1 2 3 4 5 6 | shape=(2, 3) ndim=2 size=6 dtype=float64 |
| 2 | stdin | 10 20 30 40 50 60 | shape=(3, 2) ndim=2 size=6 dtype=float64 |
Hints · 2
01Hint
np.loadtxt can read whitespace-separated rows straight from a string.
02Hint
Every ndarray exposes .shape, .ndim, .size and .dtype.