Selecting by a list of positions - reordering, repeating, and building a result whose shape follows the index.
Overview
Indexing with an array
Fancy indexing selects elements by passing an array of indices instead of a single integer or a slice. The result contains the elements at those positions, in the order the indices specify. That order can be anything. You can repeat indices, reverse them, or pick an arbitrary subset.
The key difference from slicing is that fancy indexing always returns a copy, not a view. A slice shares memory with the original array. An index array does not. That distinction matters when you assign through the result. Assigning through a slice modifies the original. Assigning through a fancy index modifies a copy, and the original is unchanged.
Fancy indexing works in multiple dimensions. For a 2-D array, two index arrays select individual elements. a[[0, 2], [1, 3]] gives the elements at (0,1) and (2,3), not a sub-matrix. Getting a sub-matrix requires np.ix_, which turns the two 1-D arrays into an open mesh that selects all combinations.
The combination of fancy indexing and boolean masking is common. You find the indices where a condition holds with np.where, and then use those indices to select from another array. np.where with a single argument returns a tuple of index arrays, one per dimension. np.where with three arguments acts as a vectorised if-else.
argsort returns indices that sort an array. You can use those indices with fancy indexing to reorder the original and any parallel arrays. That is how you sort a matrix by one column: argsort the column, then index the whole matrix with the result.
One pitfall is repeated indices in assignment. a[[0, 0, 0]] += 1 does not increment a[0] three times. It increments it once because the additions are not accumulated. np.add.at is the correct way to accumulate repeated assignments.
This module covers fancy indexing in one and multiple dimensions, the copy-versus-view distinction, np.ix_ for cross-product selection, np.where for conditional selection, and the pitfalls of repeated indices in assignment.
Give an int array where you'd most times give a number. You get those elements back:
a[[0, 2, 4]]
Order is preserved as given. So This is also how you reorder. spots may repeat, so it's also how you tile or expand. Minus indices work as usual.
The result takes the shape of the index, not the source. Indexing a 1-D array with a (2, 2) index array gives a (2, 2) result. That is at times what you want and is worth knowing before it surprises you.
Worth knowing
Indexing with an array of positions selects those elements in that order, repeats included.
The result takes the shape of the index, not of the source array.
Fancy indexing always copies. Assigning through it writes in place.
With repeated positions, a[[0,0,0]] += 1 adds once, not three times. Use np.add.at to accumulate.
Two index arrays are matched elementwise into pairs, not crossed into a rectangle — use np.ix_ for the rectangle.
argsort plus fancy indexing is how you sort one array by another and keep parallel arrays aligned.
Fancy Indexing
Selecting by a list of spots - reordering, repeating. A result whose shape follows the index.
Indexing with a list of positions
Give an array of indices and you get those elements, in that order, however many times you ask.
example_01.pyNumPy
import numpy as np
a = np.array([10, 20, 30, 40, 50])
print("a :", a)
print("a[[0, 2, 4]] :", a[[0, 2, 4]])
print("reordered :", a[[4, 3, 2, 1, 0]])
print("repeated :", a[[1, 1, 1]], "<- the same element three times")
print("negative :", a[[-1, -2]])
print()
print("The result takes the shape of the INDEX, not the source:")
idx = np.array([[0, 1], [3, 4]])
print("index shape", idx.shape, "-> result shape", a[idx].shape)
print(a[idx])
Output
It always copies
There is no stride pattern that describes arbitrary positions, so the result is new memory every time.
example_02.pyNumPy
import numpy as np
a = np.arange(6)
picked = a[[0, 2, 4]]
print("shares memory:", np.shares_memory(a, picked))
picked[0] = 99
print("a after writing to the result:", a, "<- untouched")
print()
print("But assigning through the index writes in place:")
a[[0, 2, 4]] = -1
print(" a:", a)
print()
print("Repeated positions in an assignment: last write wins, not accumulation.")
b = np.zeros(3)
b[[0, 0, 0]] += 1
print(" b[[0,0,0]] += 1 ->", b, "<- 1.0, not 3.0")
print(" np.add.at accumulates properly:")
c = np.zeros(3)
np.add.at(c, [0, 0, 0], 1)
print(" ", c)
Output
Two dimensions: pairs, not a rectangle
Index arrays for each axis are matched elementwise. That is different from slicing, and it is the usual surprise.
example_03.pyNumPy
import numpy as np
a = np.arange(12).reshape(3, 4)
print(a)
print()
rows = [0, 2]
cols = [1, 3]
print("a[[0,2], [1,3]] :", a[rows, cols], "<- (0,1) and (2,3): PAIRS")
print()
print("For the rectangle you probably meant, index in two steps:")
print(a[np.ix_(rows, cols)])
print()
print("or slice one axis and fancy-index the other:")
print(a[np.array(rows)[:, None], cols])
Output
Selecting whole rows or columns
The most common use: reordering, sampling, or taking a subset of records.
example_04.pyNumPy
import numpy as np
data = np.arange(20).reshape(5, 4)
print("data:"); print(data)
print()
print("rows 3, 0, 4 in that order:")
print(data[[3, 0, 4]])
print()
print("columns 2 and 0:")
print(data[:, [2, 0]])
print()
rng = np.random.default_rng(0)
sample = rng.choice(len(data), size=3, replace=False)
print("a random sample of rows", sample, ":")
print(data[sample])
Output
Sorting one array by another
argsort gives positions, and fancy indexing applies them — which keeps parallel arrays aligned.
example_05.pyNumPy
import numpy as np
names = np.array(["ada", "bob", "cy", "dee"])
scores = np.array([72, 95, 61, 88])
order = np.argsort(scores)[::-1] # descending
print("order :", order)
print("by score :", names[order], scores[order])
print()
print("This is why argsort exists: the same permutation applied to")
print("several arrays keeps the rows lined up.")
ages = np.array([31, 25, 40, 29])
print(" ages too:", ages[order])
Output
take, put and the fast path
np.take is fancy indexing with options, and it is usually a little quicker on large arrays.
example_06.pyNumPy
import numpy as np
import time
a = np.arange(200_000)
idx = np.arange(0, 200_000, 3)
t = time.perf_counter(); _ = a[idx]; brackets = time.perf_counter() - t
t = time.perf_counter(); _ = np.take(a, idx); take = time.perf_counter() - t
print("a[idx] : %.5f s" % brackets)
print("np.take : %.5f s" % take)
print("same result:", np.array_equal(a[idx], np.take(a, idx)))
print()
print("take can also clip or wrap instead of raising:")
small = np.array([10, 20, 30])
print(" mode='clip' :", np.take(small, [0, 5], mode="clip"))
print(" mode='wrap' :", np.take(small, [0, 5], mode="wrap"))
try:
small[[0, 5]]
except IndexError as e:
print(" plain index :", type(e).__name__, "-", str(e)[:44])
Output
It copies
Fancy indexing can't return a view. The selected spots are any. A view has to be describable as a start, a shape and a set of strides. Which scattered spots aren't.
So a[[0, 2, 4]] is new memory, and writing to it leaves the first alone.
Assigning through it's other: a[[0, 2, 4]] = -1 writes into the first in place. Same as with boolean masks. Reading copies, writing doesn't.
Repeated positions in an assignment
A sharp edge worth knowing about.
b[[0, 0, 0]] += 1
This does not add three. It reads the value once, adds one. Writes the result back three times. The answer is one.
That is a result of how boosted set is defined rather than a bug. It catches people building histograms or building up into bins.
np.add.at(b, [0, 0, 0], 1) does the building up version. It's slower. It can't use the vectorised path. It's correct. For counting namely, np.bincount is faster still.
Two dimensions
This is the main surprise.
a[[0, 2], [1, 3]]
does not select rows 0 and 2 crossed with columns 1 and 3. It pairs them elementwise and returns the two elements at (0, 1) and (2, 3).
That action is steady — the index arrays broadcast against each other, then each pair is a coord — and it isn't what most people expect the first time.
For the rectangle, there are two idioms. a[np.ix_(rows, cols)] builds the open mesh for you and is the clearest. Or reshape one index to a column so the two broadcast into a grid: a[np.array(rows)[:, None], cols].
Once you've seen that the second form is just broadcasting applied to indices, the rule stops being a special case.
Sorting by a key: argsort returns positions, and applying the same positions to several arrays keeps them aligned. That is the main reason argsort exists rather than just sort — the permutation is reusable.
take
np.take(a, idx) is fancy indexing as a function. It's often slightly faster on large arrays. It takes a mode arg that plain indexing doesn't:
mode='raise' is the default and matches a[idx]. mode='clip' pins out-of-range indices to the ends. mode='wrap' wraps them around.
Those modes are handy for boundary handling. A filter that would read past the edge, say. Where the option is padding the array or special-casing the ends.
Mixing fancy indexing with slices
Mixing the two in one formula is where fancy indexing stops being natural.
a[[0, 2], 1:3] works and does what you expect: rows 0 and 2, columns 1 to 2.
But when fancy indices appear on both sides of a slice. a[[0, 2], :, [1, 3]]. NumPy has to decide where the resulting axis goes. The rule is that the fancy-indexed axis moves to the front. The result's shape isn't the one most people predict.
The hands-on advice is to avoid the ambiguous forms. If an formula mixes fancy indices and slices in a way that makes you pause, split it into two steps. Two clear picks cost one extra mid array and save the next reader from working out the rule.
np.ix_ builds the rectangle
The most common thing people want from a[[0, 2], [1, 3]] is the four elements at the intersection of rows 0 and 2 with columns 1 and 3.
What they get is two elements: a[0,1] and a[2,3]. The index arrays are paired elementwise.
np.ix_ converts a set of index lists into the shapes that broadcast into a grid:
a[np.ix_([0, 2], [1, 3])]
gives the (2, 2) rectangle. It works by reshaping the first list to a column and the second to a row. Broadcasting makes each mix. The same trick as a[:, None] against b[None, :], packaged for readability.
The option, a[[0, 2]][:, [1, 3]], gives the same answer with two picks and two copies. np.ix_ is one pick and says what it means.
put, take and the clip modes
np.take(a, idx) is fancy indexing as a function call. It's often a little faster than bracket syntax on 1-D arrays. It accepts an axis arg. This Makes np.take(a, idx, axis=1) a clean way to select columns without a slice full of colons.
Its handy extra is mode. By default an out-of-range index raises, as with normal indexing. mode="clip" pins the index to the valid range, and mode="wrap" wraps it around.
That turns a whole class of boundary-rule loops into a single call. Sampling neighbours near the edge of an image, say. Here The choice between clamping and wrapping is a param rather than a branch.
np.put is the set counterpart, writing values at given flat spots in place.
The idiom this all exists for
Fancy indexing looks like a set of tricks until you see the one pattern it's really for:
order = np.argsort(key)
table_a = table_a[order]
table_b = table_b[order]
Find an index array once, apply it to all that needs to stay aligned. That is sorting a table by a column, taking the top n, shuffling a dataset, applying a train/test split. Reordering to match another array. All the same step.
Once you recognise it, most uses of fancy indexing in real code turn out to be an instance of it, and the rest are lookups.
Lookups
The second everyday use is treating an array as a lookup table.
labels[guesses] converts an array of class indices into an array of names. palette[image] converts an index image into colours. np.unique(..., return_inverse=True) makes just the index array that this consumes.
The pattern is values[index_array], and the result has the shape of the *index*, not of the values. That is worth stating by name. It's the opposite of the intuition built up from slicing, and it's what makes a (h, w) index array plus a (256, 3) palette give an (h, w, 3) image.
Choosing between the three ways to select
Basic slicing when the pick is a regular range. It's a view, it's free. It's the only one of the three that doesn't allocate.
Boolean masking when the pick is a rule. The mask has the shape of the data. It composes with & and |.
Fancy indexing when the pick is a list of spots, or when the order matters. Masks can't reorder or repeat. Fancy indexing can do both.
The share is smaller than it looks. A rule wants a mask. A permutation wants an index array. A contiguous window wants a slice. When two of them would work, the one that expresses the intent right is the right choice. It's often also the faster one.
Repeated indices in assignment
Selecting with repeated indices returns repeated values, which is unsurprising.
Assigning with repeated indices is where it gets notable. a[[0, 0, 1]] = [10, 20, 30] leaves element 0 holding 20. The last write wins. The earlier one is simply overwritten.
The version that catches people is the boosted form:
a[[0, 0, 1]] += 1
Element 0 is incremented once, not twice. The formula expands to a fetch, an add and a store. The fetch makes a copy holding element 0 twice, both copies get incremented. Both are written back to the same location.
np.add.at(a, [0, 0, 1], 1) is the unbuffered version that does what the syntax suggests. each ufunc has an .at method for this.
This matters in any build-up where indices repeat. Building a histogram, scattering values into bins, building up grads. The buffered version makes a plausible undercount rather than an error. This Is the worst kind of wrong.
For the exact case of counting or summing by int key, np.bincount(idx) and np.bincount(idx, weights=vals) are both correct and a lot faster than add.at.
Negative indices work
Fancy indexing accepts minus spots, counting from the end just as normal indexing does. a[[-1, -2]] takes the last two elements in reverse order.
That is convenient and at times a hazard. An index array found by subtraction that accidentally goes minus selects from the far end of the array instead of raising. A -1 made by "not found" logic silently returns the last element.
If out-of-range should be an error rather than a wrap, check the index array before using it, or use np.take with the default mode="raise". This Validates but treats negatives the same way.
The cost
Fancy indexing always allocates. The result is a new array of the index's shape. The values are gathered one by one from scattered spots.
That gathering isn't free even beyond the allocation. Scattered reads defeat the processor's cache in the way covered in the speed module. Fancy indexing a large array with a random permutation is usefully slower per element than reading it in order.
The hands-on result is small. It's still far faster than a Python loop — but it means fancy indexing inside a tight loop over a large array is worth a second look. Sorting the index array first. Here Order doesn't matter, can help by making the reads more sequential.
Questions people ask about Fancy Indexing
What is fancy indexing? Indexing with an array or list of integers instead of a slice. a[[0, 3, 5]] picks those three positions in the order you gave.
Does fancy indexing return a view? No, it always returns a copy. Slices give views and fancy indexing does not, which matters when you assign to the result.
How do two index arrays combine? They pair up element by element. a[[0, 1], [2, 3]] returns a[0, 2] and a[1, 3], not a 2 by 2 block. Use np.ix_ if you want the block.
What happens when an index repeats in an assignment? The last write wins for plain assignment. For counting use np.add.at, which applies every repeated index instead of just one.
Can I mix a slice and an index array? Yes, and the slice axis keeps its position. a[:, [0, 2]] gives all rows and columns 0 and 2.
Are negative indices allowed in the index array? Yes, they count from the end like normal indexing. Out-of-range values raise an IndexError.
The three selections, compared
Basic slicing
Boolean mask
Fancy indexing
Result
view
copy
copy
Selects by
position range
condition
explicit positions
Can reorder
only reversal
no
yes
Can repeat
no
no
yes
Result shape
derived from slice
1-D, length = matches
shape of the index
The last row is the one that surprises people most. A boolean mask always flattens to a 1-D result. But Many dims it covered. Fancy indexing takes the shape of the index array. A (h, w) index array against a (256, 3) palette makes (h, w, 3).
Choosing right is mostly a matter of matching the tool to the question. A range wants a slice, a rule wants a mask, a list of spots wants fancy indexing. Where two would work, the one that states the intent is often also the faster one.
Check yourself
0 of 4
Answer without scrolling back up.
What does `a[[0, 2], [1, 3]]` select from a 2-D array?
Index arrays are matched elementwise into coordinate pairs, not crossed. Use `np.ix_(rows, cols)` for the rectangle.
Does fancy indexing return a view?
A view needs a start, shape and strides. Scattered positions have none, so the result is new memory - though assigning through the index still writes in place.
What does `b[[0,0,0]] += 1` do to b[0]?
It reads once, adds one, and writes back three times. `np.add.at` accumulates properly; `np.bincount` is faster for counting.
Why does `argsort` exist rather than just `sort`?
The permutation is reusable - sorting names by score and applying the same order to ages keeps the rows together.
Cheat sheet
Fancy Indexing
Index a NumPy array with a list of positions to reorder, repeat or select scattered elements. See how the index shape decides the output shape.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.