Worked examples — Indexing and slicing — basic, boolean masking, fancy indexing
Throughout, an array is just numbered boxes in a row (or a grid). Indexing means "hand me the boxes at these positions." The three tools:
- Slicing
start:stop:step→ a view (a window onto the same boxes). - Boolean mask → a copy of every box where a True/False stencil shows True.
- Fancy indexing → a copy of boxes at a list of positions, in that order.
The scenario matrix
Every cell below is hit by at least one worked example. If your real code lands in a cell, the matching example shows the mechanics and the sanity check. Figure s01 draws the three tools on one row of boxes so you can see the difference before we start.

In figure s01: the cyan dashed window is a slice (a view onto the real boxes); the amber T/F row underneath is a boolean stencil; the white arrow is a fancy index list picking positions out of order. Keep this picture in mind — every example is one of these three, or a combination.
| Cell | Case class | Covered by |
|---|---|---|
| A | Positive-step slice, count formula | Ex 1 |
| B | Negative step (reverse) + omitted bounds a[:5], a[5:], a[::-1] |
Ex 2 |
| C | Degenerate slice → empty result + zero-step error | Ex 3 |
| D | Out-of-bounds slice → silent clamping | Ex 4 |
| E | 2-D slice = block; view side-effect | Ex 5 |
| F | Boolean mask + combining with &, ~ |
Ex 6 |
| G | In-place assignment through a mask | Ex 7 |
| H | Fancy indexing: reorder, repeat, negative index | Ex 8 |
| I | 2-D fancy: pointwise zip vs true block (np.ix_) |
Ex 9 |
| J | Mixed-mode multi-D: fancy + slice, mask on a 2-D array | Ex 10 |
| K | Real-world word problem (sensor data) + dtype pitfall | Ex 11 |
| L | Exam twist: view-vs-copy chained assignment | Ex 12 |
Ex 1 — Cell A · positive-step slice & the count formula
Steps
-
Recall the count formula from the parent note. For step : Why this step? We need to know how many boxes come out before listing them — it stops us miscounting the last index. ( is the round-up defined above.)
-
Plug in : Why this step? is not whole, so we round up — there's a partial "gap" that still fits one more index. Ceiling captures exactly that.
-
List the indices . The next would be → excluded. Why this step?
stopis exclusive (up to but not including). Index 15 is the last one strictly below 17.
Answer:
a[3:17:4] → array([3, 7, 11, 15]), count .
Recall Verify
so 19 is correctly dropped; and matches the 4 listed values. ✓
Ex 2 — Cell B · negative step + omitted bounds
Steps
-
Omitted bounds fill in the defaults. A missing
startmeans ; a missingstopmeanslen(a)(here 20); a missingstepmeans . So:a[:5]=a[0:5:1]→[0,1,2,3,4](first 5).a[5:]=a[5:20:1]→[5,6,…,19](everything from index 5 on, 15 elements). Why this step? Most real slicing omits bounds; you must know what NumPy silently fills in. See figure s02, panel (a): the cyan window snaps to the array edge whenever a bound is blank.
-
a[::-1]reverses. Withstep = -1and both bounds omitted, NumPy uses the reverse defaults: start = last index (19), stop = "just past the front". So it walks19,18,…,1,0. Why this step? This is the idiom for reversing an array. The defaults flip because a negative step must start high and end low — figure s02, panel (b) shows the amber arrow running right-to-left. -
a[15:3:-4]— an explicit negative step. For a negative step the count formula flips: we count integers with (strictly greater, since we go down andstopis still exclusive). Using and the defined above: Why this step? Withstep<0the plain would be negative; we reframe by measuring the downward distance (its size via ), and clamp in case that distance is itself negative (a wrong-direction range — see Ex 3). -
List going down by 4 from 15: . Next is → excluded. Why this step? Figure s02, panel (c) — the amber arrow hops left and must stop before the box labelled 3.

Answer: (a)
[0,1,2,3,4]; (b)[5,6,…,19](15 elements); (c)[19,18,…,1,0](reversed, 20 elements); (d)[15,11,7], count 3.
Recall Verify
len(a[:5])==5, len(a[5:])==15, a[::-1] first element is 19 & last is 0 with length 20, and a[15:3:-4]==[15,11,7] (count ). ✓
Ex 3 — Cell C · degenerate / empty slice + zero-step error
Steps
-
a[5:5]: . Count . Why this step? No index satisfies . Zero boxes fit. -
a[8:3]: default step , but . Count formula gives , then . Why this step? Walking upward from 8 can never reach below 3, so nothing qualifies. The clamp is why negatives become empty, not errors. -
a[3:8:-1]: step means walk down from 3, but is above 3. Downward distance . Why this step? You'd need to go up to reach 8, but the step says go down. Impossible → empty. -
a[::0]: a step of zero is illegal — it means "advance by nothing", so the walk never moves and can never terminate. Python raisesValueError: slice step cannot be zero. Why this step? Every other degenerate slice returns an empty array silently, but a zero step is the one slicing case that actually raises. Distinguishing "empty vs error" here is the key takeaway.
Answer:
a[5:5],a[8:3],a[3:8:-1]are allarray([], dtype=int64)— length 0.a[::0]raisesValueError: slice step cannot be zero.
Recall Verify
All three ordinary slices clamp to count 0; len(a[5:5]) == len(a[8:3]) == len(a[3:8:-1]) == 0, and a[::0] raises ValueError. ✓
Ex 4 — Cell D · out-of-bounds slice → silent clamping
Steps
-
Slices clamp bounds to the array; they never raise for being out of range. Any
stopabovelen(a)is treated aslen(a); anystartbelow-len(a)is treated as the front. So:a[15:999]→stopclamped to 20 →[15,16,17,18,19].a[-999:3]→startclamped to the front (0) →[0,1,2].a[18:25]→stopclamped to 20 →[18,19]. Why this step? This is the key difference from scalar indexing.a[25]raisesIndexError, buta[18:25]politely gives whatever exists. This is why "grab the next 5 starting here" is safe near the end — you just get fewer if there aren't 5 left.
-
Sanity: the count of
a[15:999]ismin(999, 20) - 15 = 5; ofa[-999:3]is3 - max(-999, -20 → 0) = 3; ofa[18:25]ismin(25,20) - 18 = 2. Why this step? Mentally clamp each bound into first, then apply the ordinary count formula.
Answer:
a[15:999] → [15,16,17,18,19],a[-999:3] → [0,1,2],a[18:25] → [18,19]. None raise.
Recall Verify
len(a[15:999])==5, len(a[-999:3])==3, len(a[18:25])==2, and none raise. ✓
Ex 5 — Cell E · 2-D block slice & the view side-effect
Steps
-
Build
A: values0..11in a 3×4 grid (row-major). So row 0 is[0,1,2,3], row 1 is[4,5,6,7]. Why this step? We must know exactly which boxes rows {0,1} × cols {1,2} pick. -
A[0:2, 1:3]: rows 0-1, cols 1-2 → the block Why this step? Both indices are ranges, so we intersect them into a rectangle — see the amber block in figure s03. -
Bis a view: a range-slice only changes the offset/strides recipe, no data copied. SoB[0,0] = 99writes into the shared buffer atA's position (0,1). Why this step? This is the parent note's Trap 1 — NumPy slices are not independent copies like Python lists.

Figure s03: the amber cells are the sliced block [[1,2],[5,6]]; the cyan dashed rectangle marks the rows/cols the two ranges select. Because it's a view, editing an amber cell rewrites the matching cell of A.
Answer:
B → [[99, 2],[5, 6]]andA[0,1] == 99(A is mutated!).
Recall Verify
After the write, A row 0 is [0, 99, 2, 3], so A[0,1] == 99 and B[0,0] == 99. ✓
Ex 6 — Cell F · boolean mask with & and ~
Steps
-
Build the two elementwise conditions:
c1 = a > 0andc2 = a <= 5. Why this step? Each is a boolean array the same shape asa— a True/False stencil, not a single yes/no. -
Combine with
&(elementwise AND), each condition in parentheses:
mask = (a > 0) & (a <= 5)Why & not and? and needs one boolean; here both sides are 8-long arrays. & ANDs them box-by-box. Parentheses are required because & binds tighter than >.
- Evaluate per element:
| a | >0 |
<=5 |
& |
|---|---|---|---|
| 3 | T | T | T |
| -1 | F | T | F |
| 4 | T | T | T |
| -2 | F | T | F |
| 5 | T | T | T |
| 0 | F | T | F |
| -7 | F | T | F |
| 8 | T | F | F |
Why this step? A mask returns a 1-D copy of the True cells, in order → [3, 4, 5].
- Complement
a[~mask]: the~operator flips every entry ofmask(True↔False), so~maskis True exactly wheremaskwas False. Reading the table,maskis False for the elements-1, -2, 0, -7, 8— so those are precisely the ones the complement collects. Why this step? The mask and its complement partition the array: every element is in exactly one of the two selections. Note5hadmask = True, so it stays in the first group and is absent from the complement.
Answer:
a[mask] → array([3, 4, 5]);a[~mask] → array([-1, -2, 0, -7, 8]).
Recall Verify
The two selections partition all 8 elements: and together (as multisets) they equal the whole array. ✓
Ex 7 — Cell G · in-place assignment through a mask
Steps
-
Build the mask
a < 0→[F, T, F, T, F, T]. Why this step? We want to target exactly the negative boxes. -
Assign into the masked positions:
a[a < 0] = 0Why this step? When a mask sits on the left of =, NumPy writes the right-hand value into every True cell — a vectorized "set these to 0", no loop.
- Only the True positions change; positives are untouched.
Why this step? The stencil covers cells 1, 3, 5 → those become 0; cells 0, 2, 4 keep
3, 4, 5.
Answer:
a → array([3, 0, 4, 0, 5, 0]).
Recall Verify
Sum before was ; after clipping negatives to 0 the sum is , and no positive value changed. ✓
Ex 8 — Cell H · fancy indexing: reorder, repeat, negative
Steps
-
Fancy indexing takes an integer list of positions and returns a copy with elements in that exact order, repeats allowed. Why this step? Unlike a slice (a regular stride), we're naming arbitrary positions, so NumPy must gather them into fresh memory.
-
Resolve each index against
a(length 5). Negative-1means "last", i.e. position 4. Why this step? Negative fancy indices follow the same "count from end" rule as basic indexing. -
Read off: pos 4 → 50, pos 0 → 10, pos 0 → 10 (repeat), pos -1 → 50, pos 2 → 30. Why this step? The result shape matches the index list (length 5), not
a's shape — here both are 5, but see Ex 9 for the general rule.
Answer:
a[[4, 0, 0, -1, 2]] → array([50, 10, 10, 50, 30]).
Recall Verify
Length of output (matches index list); value at output position 3 is a[-1] == 50. ✓
Ex 9 — Cell I · 2-D fancy: pointwise zip vs true block
Steps
-
Recall
A(3×4): row 0[0,1,2,3], row 1[4,5,6,7], row 2[8,9,10,11]. Why this step? We must locate specific(row, col)cells. -
A[[0,2],[1,3]]zips the two index arrays pointwise: index 0 → (row 0, col 1), index 1 → (row 2, col 3). Why this step? This is the parent's Trap 2 — two fancy index arrays are paired element-by-element, giving a 1-D result of length 2, not a rectangle. -
For the actual block of rows {0,2} × cols {1,3}, use
np.ix_, which reshapes the indices to broadcast into a grid:
A[np.ix_([0,2],[1,3])]Why this step? np.ix_ turns [0,2] into a column and [1,3] into a row so they broadcast to a 2×2 grid of all combinations — see figure s04.

Figure s04: the amber cells 1 and 11 are the pointwise zip A[[0,2],[1,3]] (a diagonal pick); the cyan cells 1, 3, 9, 11 are the full block from np.ix_. Notice the zip is just the block's main diagonal.
Answer:
A[[0,2],[1,3]] → array([1, 11])(the amber diagonal);A[np.ix_([0,2],[1,3])] → [[1,3],[9,11]](the cyan block).
Recall Verify
The zip gives exactly the two corners 1 and 11, which are the block's main diagonal; the block additionally contains 3 and 9. ✓
Ex 10 — Cell J · mixed-mode multi-D indexing
Steps
-
(a) fancy + slice. Axis 0 gets the fancy list
[0,2](pick rows 0 and 2); axis 1 gets the slice1:3(cols 1,2). NumPy applies each axis independently: each chosen row is sliced to its cols 1-2, giving a2 × 2result. Why this step? When one axis is fancy and another is a plain slice, they don't "zip" — the slice acts like a normal column-window on every fancy-selected row. Result: -
(b) 2-D boolean mask.
A > 6is a3×4True/False grid;A[mask]gathers every True cell into a 1-D copy, row-major. The values are7,8,9,10,11. Why this step? A boolean mask over a 2-D array always flattens — the True cells don't form a rectangle, so there's no shape to keep (parent's Trap 4). Result:array([7, 8, 9, 10, 11]), shape(5,). -
(c) slice + fancy on axis 1.
:keeps all 3 rows;[3,0]reorders columns to (last, first). Each row becomes[row[3], row[0]], giving a3 × 2result: Why this step? Fancy indexing on a single axis just reorders/selects along that axis while the sliced axis stays full — a common "column reshuffle" idiom.
Answer: (a)
[[1,2],[9,10]], shape(2,2); (b)[7,8,9,10,11], shape(5,)(flattened!); (c)[[3,0],[7,4],[11,8]], shape(3,2).
Recall Verify
(a) shape (2,2) with values [[1,2],[9,10]]; (b) flattened to [7,8,9,10,11]; (c) shape (3,2) with values [[3,0],[7,4],[11,8]]. ✓
Ex 11 — Cell K · real-world word problem (sensor data) + dtype pitfall
Steps
-
Build a "valid" mask
valid = t != -999. Why this step? We must exclude sentinel values before doing any statistics — averaging them would poison the result. -
Count valid: there are 2 flags among 10 → valid. Why this step?
valid.sum()counts Trues (True ). -
Mean of valid =
t[valid].mean(). The valid values are21,19,23,22,25,20,27,24; their sum is Why this step?t[valid]is a boolean-mask copy of just the good readings, so.mean()ignores the flags. -
Dtype pitfall.
twas built from integer literals, so its dtype is integer (int64). Writing a float into an integer array does not upgrade the array — NumPy casts the value down toint, truncating the fraction:
t[t == -999] = round(22.625, 1) # you MEANT 22.6 ...
# ... but t is int64, so 22.6 is truncated to 22 on write!Why this step? An array's dtype is fixed at creation; assignment converts the value to fit, it never changes the array's type. So the flagged cells actually become 22, not 22.6 — a silent data-corruption bug.
- The fix. To keep the decimal, make the array float first:
t = t.astype(float) # now dtype float64
t[t == -999] = round(22.625, 1) # stores 22.6 correctlyWhy this step? A float64 array can hold 22.6 exactly enough for our purposes, so the fill is preserved.
Answer: (a) 8 valid readings. (b) mean °C. (c) On the original int array, each
-999becomes 22 (the float is truncated!). Only aftert.astype(float)do the flags become 22.6.
Recall Verify
valid flags total; ; on an int array int(22.6) == 22 (truncated fill); after astype(float) the stored fill is 22.6. ✓
Ex 12 — Cell L · exam twist: view-vs-copy chained assignment
Steps
-
Recall
A: row 0[0,1,2], row 1[3,4,5]. The conditionA > 2is True for3, 4, 5(all of row 1). Why this step? We need to know which cells the mask selects before reasoning about the write. -
Attempt 1 — Python evaluates
A[A > 2]first. A boolean mask returns a copy (a brand-new temporary array[3, 4, 5]). Then[0] = 100writes100into element 0 of that temporary copy. Why this step? Because masking makes a copy, the write lands in an object that nobody keeps a reference to — the temporary is discarded the instant the line finishes.Ais never touched. -
So after attempt 1,
Ais still[[0,1,2],[3,4,5]](sum ), completely unchanged. Why this step? This is the "chained indexing on a copy" gotcha — the assignment succeeds but modifies a ghost. -
Attempt 2 — here the mask is on the left of a single
=. NumPy readsA[mask] = valueas one atomic "set these cells" operation and writes directly intoA's real buffer. Why this step? Left-side masking is assignment, not read-then-write; there is no intermediate copy to escape into. The True cells3, 4, 5all become100. -
So after attempt 2 (on a fresh
A),Ais[[0,1,2],[100,100,100]], sum . Why this step? Only row 1 was> 2, so exactly those three cells change.
Answer: Attempt 1 does nothing to
A(writes into a discarded copy,Asum stays 15). Attempt 2 setsA → [[0,1,2],[100,100,100]](sum 303).
Recall Verify
Attempt 1 leaves A as arange(6).reshape(2,3) (sum ). Attempt 2 on a fresh A gives sum . ✓
Recall One-line recap of every cell
Slice count (Ex1); omitted bounds default to front/end and a[::-1] reverses, negative step flips to (Ex2); impossible ranges clamp to empty while step=0 errors (Ex3); out-of-bounds slices clamp silently (Ex4); 2-D slices are views that leak edits (Ex5); masks combine with & ~ (Ex6) and assign in place (Ex7); fancy = reorder+repeat+negative (Ex8); two fancy arrays zip — use np.ix_ for a block (Ex9); mixing fancy with a slice gives a block and a 2-D mask flattens (Ex10); clean sensor data by masking flags but mind the int→float dtype trap (Ex11); never edit a chained-mask ghost (Ex12).
Connections
- Parent: Indexing & slicing
- Views vs Copies — memory model (Ex 5 & Ex 12 hinge on this)
- np.where and conditional selection (a cleaner alternative to Ex 7 / Ex 11)
- Vectorization — replacing Python loops (why mask-assignment beats a
for) - Broadcasting (how
np.ix_in Ex 9 works under the hood) - NumPy arrays — shape, strides, dtype (why the dtype in Ex 11 is fixed at creation)
- Pandas .loc / .iloc indexing