Worked examples — NumPy — ndarray structure, dtype, shape, strides
You have met the parent idea: an array is one flat row of bytes plus three bookkeeping numbers. Here we prove we understand it by walking every situation you could ever be handed. Not "here is a formula" — here is every kind of question the formula must survive.
We rely on three earned facts from the parent. Before using anything, re-say it in plain words:
Recall The three tools we will use over and over
dtype ::: the "fatness" of one number — how many bytes it occupies. int64 = 8 bytes.
shape (d0, d1, …) ::: how many entries pretend to live along each direction (axis).
strides ::: how many bytes you jump to move one step along an axis. Units are bytes, never elements.
byte offset formula ::: to find element at indices , compute . This is the single machine we test below.
The scenario matrix
Every question about ndarray memory falls into one of these cells. The worked examples that follow each carry a [cell] tag so you can see we left no gap.
| Cell | What makes it tricky | Covered by |
|---|---|---|
| A. Plain C-order | the "normal" row-major case | Ex 1 |
| B. dtype changes the step | strides scale with itemsize | Ex 2 |
| C. Transpose (F-order) | strides swap, no copy | Ex 3 |
| D. Slice = view (aliasing danger) | offset base + shared memory | Ex 4 |
| E. Non-contiguous / step slice | stride becomes a multiple | Ex 5 |
| F. Degenerate: 1-D & size-1 axis | stride of a length-1 axis is "don't care" | Ex 6 |
| G. Zero-size / empty array | limiting case, product = 0 | Ex 7 |
| H. reshape that MUST copy | when the layout is impossible | Ex 8 |
| I. Word problem (real data) | image rows/columns, pick the axis | Ex 9 |
| J. Exam twist: negative stride | reversed views | Ex 10 |
Example 1 — the plain C-order case [cell A]
Forecast first. Before reading on, guess the two stride numbers. (Hint: itemsize is 8.)
- Itemsize.
int64⇒ each element is bytes. Why this step? Strides are measured in bytes, so we always start by pinning down the fatness of one element. - Last-axis stride. Neighbours along the last axis (columns) sit right next to each other in memory, so . Why? The parent built the buffer row by row — within a row consecutive columns are adjacent.
- First-axis stride. To drop one row you skip a whole row of elements: . Why? Formula ; for the only later axis has size 4.
- Offset of
A[2,1]. bytes. Why? Apply .
Look at the figure: the flat shelf of books, and the two "jump sizes" drawn as arrows.

Verify. A.strides == (32, 8). Element index , and independently the flat index is . Both give A[2,1] == 9. ✓
Example 2 — dtype changes the step [cell B]
Forecast. Half the fatness of int16 vs int64? Actually int16 = 2 bytes. Guess before continuing.
- Itemsize.
int16⇒ bytes. Why? "16" means 16 bits; 8 bits per byte ⇒ 2 bytes. - Strides. , . Why? The shape is identical to Ex 1, so the pattern is identical — only the scale factor shrank. This is the whole point of measuring strides in bytes: change the dtype, and the strides recompute automatically.
- nbytes. bytes.
Verify. B.strides == (8, 2), B.nbytes == 24. Notice this equals A.nbytes from Ex 1? No — A was int64 (96 bytes). Same count, different bytes. ✓
Example 3 — transpose swaps strides, moves nothing [cell C]
Forecast. Transpose flips rows and columns — does it re-lay the memory?
- New shape. . Swap. Why? Transpose relabels which axis is "rows".
- New strides = old strides, swapped.
At.strides == (8, 32). Why? No byte moved. To move down a row ofAt(which is a column ofA) you must step byA's old column-to-column-in-next-row jump, i.e. its old stride_0 = 32. NumPy just relabels the jump sizes. - Offset of
At[1,2]. bytes. Why? Same with the swapped strides.

Verify. Offset 72 ⇒ element index . And At[1,2] == A[2,1] == 9. The transpose reads the same byte. np.shares_memory(A, At) is True, and At is F_CONTIGUOUS. ✓
Example 4 — a slice is a view; mutation leaks [cell D]
Forecast. Python list slices copy — does NumPy?
subis a view. Its data pointer starts partway intoA's buffer: at offset bytes (skip row 0). Why? Row-slicing changes only the starting offset and the axis-0 length; it does not copy.- Strides unchanged.
sub.strides == (32, 8)— same asA. Why? The layout inside the block is untouched; you only chose a window. sub[0,0]isA[1,0]. Writing 99 writes intoA's buffer.
Verify. A[1,0] == 99 after the write. np.shares_memory(sub, A) is True. To be safe you would use A[1:3,:].copy(). ✓
Example 5 — step-slicing multiplies the stride [cell E]
Forecast. Taking every 2nd column — does the column-stride stay 8?
- Shape. Columns 0,2 out of 0,1,2,3 ⇒ 2 columns.
C.shape == (3, 2). - Column stride doubles. Stepping "one column of
C" means stepping two real columns: bytes. SoC.strides == (32, 16). Why? A step-kslice multiplies that axis's stride byk. The array is now non-contiguous — its last-axis stride (16) is no longer equal to itemsize (8). - Consequence.
C.flags['C_CONTIGUOUS']isFalse. Why? C-contiguity demands the last-axis stride equals itemsize; 16 ≠ 8.
Verify. C.strides == (32, 16), C.shape == (3, 2), and C[0].tolist() == [0, 2]. ✓
Example 6 — degenerate: 1-D array and a length-1 axis [cell F]
Forecast. What stride does an axis of length 1 get?
- 1-D strides.
v.strides == (8,). One axis, contiguous, step = itemsize. Why? A 1-D array is both C- and F-contiguous — there is only one direction to walk. - Length-1 axis.
w.strides == (32, 8). Why? Axis 0 has size 1, so you can never actually take a step along it — its stride value is a formula output () but you'll never multiply it by any . It is a "don't care" that broadcasting later exploits (see NumPy — broadcasting).
Verify. v.strides == (8,) and w.strides == (32, 8). The size-1 axis is why w can be broadcast against a (3,4) array. ✓
Example 7 — the empty / zero-size limit [cell G]
Forecast. Shape has a 0 in it — is this even legal?
- Size is the product of the shape. .
Why?
size = product of shape; any zero factor kills it. Zero elements is perfectly legal — it is the limiting case of "reshape to nothing." - nbytes. bytes. No buffer content to store.
- shape still valid.
E.shape == (0, 4)— the "4" is retained metadata even with no rows, so future concatenation knows the column count.
Verify. E.size == 0, E.nbytes == 0, E.shape == (0, 4). ✓
Example 8 — the reshape that is FORCED to copy [cell H]
Forecast. Parent said reshape is usually free. Is it free here?
Atis non-contiguous. Its strides(8,32)do not decrease with axis index, so its logical row-major order (0,4,8,1,5,9,…) is not the physical buffer order (0,1,2,…). Why? Flattening must produce elements in logical order; but no single new stride can reproduce that scrambled order over the existing buffer.- So NumPy copies.
Rgets a fresh contiguous buffer. Why? The requested 1-D layout is impossible as a view — this is the one exception the parent warned about.
Verify. np.shares_memory(R, A) is False, while R.tolist() == [0,4,8,1,5,9,2,6,10,3,7,11] (transpose order, contiguous copy). ✓
Example 9 — word problem: a grayscale image [cell I]
Forecast. Vertical neighbours — near in the picture. Near in memory?
- Itemsize.
uint8⇒ byte. - Strides of the image. Shape , C-order: , bytes. Why? Moving down one row skips a whole row of 1920 pixels.
- The vertical strip
img[:, 1000]. Stepping "one pixel down" uses stride_0 = 1920 bytes. That is far — the strip is not contiguous; each pixel sits a full row apart. Why? Row-major storage makes horizontal neighbours cheap and vertical neighbours expensive. This is exactly why column access is cache-unfriendly and why C vs Fortran order matters for performance.
Verify. For img of shape (1080,1920) uint8, img.strides == (1920, 1); the column view img[:, 1000].strides == (1920,). ✓
Example 10 — exam twist: negative stride (reversed view) [cell J]
Forecast. Reversing — surely that must copy to flip the order?
- Negative stride.
r.strides == (-8,). Why? NumPy starts the data pointer at the last element and steps backward 8 bytes each index. The offset formula still holds: with a negative stride, increasing decreases the byte address. - No copy.
ris a view; only the start offset and the sign of the stride changed. Why? The same bytes, read right-to-left. This is the elegant edge case — the formula never assumed the strides were positive.
Verify. r.strides == (-8,), np.shares_memory(r, v) is True, and r.tolist() == [4,3,2,1,0]. ✓
Recall One-line summary of the whole matrix
The single formula survives EVERY case ::: bigger/smaller dtype (scale ), transpose (swap strides), slice (shift start), step-slice (multiply stride), length-1 axis (unused stride), empty (product 0), forced copy (impossible layout), and negative stride (reversed).
Connections
- NumPy — ndarray structure, dtype, shape, strides (Hinglish) — the parent idea in Hinglish
- NumPy — views vs copies & np.shares_memory — Ex 4, 8, 10 hinge on this
- NumPy — broadcasting — the length-1 stride trick (Ex 6)
- NumPy — vectorization & performance
- CPU cache & memory locality — why Ex 9's column is slow
- Row-major vs column-major (C vs Fortran order) — Ex 3's F-contiguity
- Python lists vs arrays — why list slices copy but array slices don't (Ex 4)