5.4.1 · D3Scientific Computing (Python)

Worked examples — NumPy — ndarray structure, dtype, shape, strides

2,372 words11 min readBack to topic

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.)

  1. 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.
  2. 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.
  3. First-axis stride. To drop one row you skip a whole row of elements: . Why? Formula ; for the only later axis has size 4.
  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.

Figure — NumPy — ndarray structure, dtype, shape, strides

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.

  1. Itemsize. int16 bytes. Why? "16" means 16 bits; 8 bits per byte ⇒ 2 bytes.
  2. 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.
  3. 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?

  1. New shape. . Swap. Why? Transpose relabels which axis is "rows".
  2. New strides = old strides, swapped. At.strides == (8, 32). Why? No byte moved. To move down a row of At (which is a column of A) you must step by A's old column-to-column-in-next-row jump, i.e. its old stride_0 = 32. NumPy just relabels the jump sizes.
  3. Offset of At[1,2]. bytes. Why? Same with the swapped strides.
Figure — NumPy — ndarray structure, dtype, shape, 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?

  1. sub is a view. Its data pointer starts partway into A'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.
  2. Strides unchanged. sub.strides == (32, 8) — same as A. Why? The layout inside the block is untouched; you only chose a window.
  3. sub[0,0] is A[1,0]. Writing 99 writes into A'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?

  1. Shape. Columns 0,2 out of 0,1,2,3 ⇒ 2 columns. C.shape == (3, 2).
  2. Column stride doubles. Stepping "one column of C" means stepping two real columns: bytes. So C.strides == (32, 16). Why? A step-k slice multiplies that axis's stride by k. The array is now non-contiguous — its last-axis stride (16) is no longer equal to itemsize (8).
  3. Consequence. C.flags['C_CONTIGUOUS'] is False. 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. 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.
  2. 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?

  1. 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."
  2. nbytes. bytes. No buffer content to store.
  3. 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?

  1. At is 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.
  2. So NumPy copies. R gets 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?

  1. Itemsize. uint8 byte.
  2. Strides of the image. Shape , C-order: , bytes. Why? Moving down one row skips a whole row of 1920 pixels.
  3. 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?

  1. 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.
  2. No copy. r is 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)