Fancy indexing
Indexing with a list of positions — and why two lists zip instead of crossing.
a[[0, 2]]a[rows, cols]np.ix_np.takenp.add.atalways copiesWatch it happen
Play it through, or step back and forth yourself.
v[3, 0, 0, 1]v[[3, 0, 0, 1]]Instead of a range, hand the brackets a list of positions. v[[3, 0, 0, 1]] takes position 3, then 0, then 0 again, then 1. Any order you like, repeats allowed — and the result has the shape of the index list, not of v.
The idea
The third way to index. Instead of a single position or a range, hand the brackets an array of positions:
v[[3, 0, 0, 1]] # position 3, then 0, then 0 again, then 1Any order you like, repeats allowed, and the result takes the shape of the index array rather than of v. That's what makes it the tool for reordering, sampling and shuffling: x[rng.permutation(len(x))] shuffles an array, and x[np.argsort(keys)] sorts one array by another.
Two index arrays zip — they don't cross
This is the part everyone gets wrong once. On a 2-D array:
a[[0, 2]] # rows 0 and 2 — shape (2, 4)
a[[0, 2], [1, 0]] # elements (0,1) and (2,0) — shape (2,)The second one is not "rows 0 and 2, columns 1 and 0". The two lists are paired up positionally, like zip: first row index with first column index, second with second. You asked for two coordinates, so you get two values.
If you wanted the 2×2 sub-grid, say so:
a[np.ix_([0, 2], [1, 0])] # shape (2, 2)
a[[0, 2]][:, [1, 0]] # same result, one extra copy
Why zipping is the sensible rule
It looks arbitrary until you know the mechanism: the index arrays broadcast against each other, by the same rules as lesson 13. Two (2,) arrays broadcast to (2,), which pairs them elementwise. Give the first one a second axis and it stretches into a grid instead:
rows = np.array([0, 2])
a[rows[:, None], [1, 0]] # (2,1) against (2,) -> (2,2)That's precisely what np.ix_ builds for you. Once you see it as broadcasting, the behaviour stops being a special case and becomes the only consistent option.
It always copies
Unlike basic slicing, fancy indexing always returns a copy. Rows 0 and 2 aren't evenly spaced, so no stride describes them — there is nothing to make a view out of.
Which leads to a subtle trap when the same position appears twice:
x = np.zeros(3)
x[[0, 0, 0]] += 1
x # [1. 0. 0.] — not [3. 0. 0.]x[idx] += 1 is really "fetch, add one, write back". The fetch happens once, produces [0, 0, 0], adds one to give [1, 1, 1], and writes all three into position 0 — so the last write wins. When you genuinely want to accumulate, use the unbuffered form:
np.add.at(x, [0, 0, 0], 1) # [3. 0. 0.]That comes up whenever you're building a histogram by hand or scattering values into bins.

Assignment
Fancy indexing works on the left-hand side too, and there it does write into the original:
a[[0, 2]] = 0 # zero out rows 0 and 2
a[[0, 2], [1, 0]] = -1 # set two specific elementsReading gives you a copy; writing edits the array. Not a contradiction — the copy is the result of a read, and there's no result to copy when you're assigning.
Practice
Write it yourself. The answer is there when you want it.
Putting the kettle on…
Starting up…
Write it yourself
not gradedIndex v with the list [3, 0, 0, 1], and a with [0, 2], printing both. Then show the difference between a[[0, 2], [1, 0]], which zips the lists into two elements, and np.ix_, which crosses them into a block. Finish with the duplicate trap: zeros of length 3, x[[0, 0, 0]] += 1, then print x.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
From v, return the values at positions 2, 0 and 3, in that order.
Return rows 0 and 3 of cups, stacked.
Return just two elements of a: the one at (0, 3) and the one at (2, 0). The result should be a 1-D array of length 2.
Now the sub-grid: rows 0 and 2 crossed with columns 3 and 0, shape (2, 2).
