Skip to main content
NumPy · lesson 4 of 13
NumPy beginner Lesson 4 of 13

NumPy Indexing and Slicing

Index, slice, mask and fancy-index NumPy arrays, and learn exactly which operations return views and which return copies, proven with np.shares_memory.

By SkillByExample · Updated

Single Elements, Rows and Columns

An index per axis, separated by commas, picks one element; a colon keeps a whole axis.

import numpy as np

m = np.array([[1, 2, 3, 4],
              [5, 6, 7, 8],
              [9, 10, 11, 12]])

print(m[0, 0], m[2, 3], m[-1, -1])
print(m[1])
print(m[:, 2])
$ python basic_index.py
1 12 12
[5 6 7 8]
[ 3  7 11]

m[1] is row 1 and m[:, 2] is column 2 — both come back 1-D. Negative indices count from the end, so m[-1, -1] and m[2, 3] are the same corner element.

Slicing

start:stop:step works on every axis, with stop excluded as in Python.

import numpy as np

arr = np.arange(0, 100, 10)
print(arr[2:6], arr[::3], arr[::-1][:3])

m = np.arange(16).reshape(4, 4)
print(m)
print(m[1:3, 1:3])
$ python slicing.py
[20 30 40 50] [ 0 30 60 90] [90 80 70]
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]]
[[ 5  6]
 [ 9 10]]

m[1:3, 1:3] takes rows 1–2 and columns 1–2, giving the 2 × 2 block from the middle. A negative step reverses: arr[::-1][:3] is the last three values, largest first.

Views and Copies

Whether an indexing result shares memory with the original is the single most important thing to know about NumPy indexing.

import numpy as np

m = np.arange(16).reshape(4, 4)

sliced = m[1:3, 1:3]
fancy = m[[1, 2]]
masked = m[m > 10]

print("slice  shares memory:", np.shares_memory(m, sliced))
print("fancy  shares memory:", np.shares_memory(m, fancy))
print("mask   shares memory:", np.shares_memory(m, masked))

sliced[0, 0] = 99
fancy[0, 0] = -1
print(m[1])
$ python views_copies.py
slice  shares memory: True
fancy  shares memory: False
mask   shares memory: False
[ 4 99  6  7]

The slice is a view, so writing 99 into it changed m[1, 1]. The fancy-indexed and masked results are copies: -1 went into fancy only, and m’s row 1 shows no trace of it. The rule: integers and slices give views; integer lists and boolean masks give copies.

Other Views: reshape, ravel and transpose

import numpy as np

a = np.arange(6)
r = a.reshape(2, 3)
t = r.T
flat = t.ravel()

print(np.shares_memory(a, r), np.shares_memory(a, t))
print(np.shares_memory(a, flat))
print(np.shares_memory(a, a.flatten()))
$ python other_views.py
True True
False
False

reshape and .T only change how the same bytes are read, so both are views. ravel returns a view when it can, but t is transposed — its elements aren’t in memory order — so here it had to copy. flatten always copies. When you need independence, call .copy() rather than guessing.

Writing Through a Copy by Accident

import numpy as np

arr = np.array([5, 12, 3, 18, 7])

arr[arr > 10][0] = 0
print(arr)

arr[arr > 10] = 0
print(arr)
$ python chained.py
[ 5 12  3 18  7]
[5 0 3 0 7]

The first assignment silently did nothing. arr[arr > 10] built a copy, [0] = 0 wrote into that copy, and the copy was discarded. A single indexing operation on the left-hand side — the second form — assigns into arr itself.

Boolean Masks

import numpy as np

temps = np.array([22.1, 35.4, 18.9, 41.2, 28.7, 15.3, 38.1])

print(temps > 30)
print(temps[temps > 30])
print(temps[(temps >= 20) & (temps <= 30)])
print(np.where(temps > 30, "hot", "ok"))

temps[temps > 40] = 40.0
print(temps.max())
$ python masks.py
[False  True False  True False False  True]
[35.4 41.2 38.1]
[22.1 28.7]
['ok' 'hot' 'ok' 'hot' 'ok' 'ok' 'hot']
40.0

A comparison produces a boolean array of the same shape, and indexing with it keeps the True positions. np.where(cond, a, b) picks elementwise between two values. Assigning through a mask (the last two lines) modifies the original, clamping 41.2 down to 40.0.

and/or Raise an Error

import numpy as np

temps = np.array([22.1, 35.4, 18.9])
try:
    print(temps[(temps > 20) and (temps < 30)])
except ValueError as e:
    print("ValueError:", e)
$ python and_error.py
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

and needs a single True or False, and a three-element boolean array has no single truth value. & and | compare element by element; the parentheses matter because & binds more tightly than >.

Fancy Indexing

import numpy as np

prices = np.array([9.99, 14.99, 4.99, 24.99, 7.49, 19.99])
print(prices[[0, 2, 5]])
print(prices[[3, 0, 3]])

data = np.arange(1, 13).reshape(4, 3)
rows = np.array([0, 1, 2])
cols = np.array([2, 0, 1])
print(data[rows, cols])
$ python fancy.py
[ 9.99  4.99 19.99]
[24.99  9.99 24.99]
[3 4 8]

An index list can pick any positions in any order, including repeats (3 twice). With two index arrays, NumPy pairs them up — data[0, 2], data[1, 0], data[2, 1] — rather than taking every combination.

Repeated Indices and +=

import numpy as np

counts = np.zeros(3, dtype=int)
counts[[0, 0, 2]] += 1
print(counts)

counts = np.zeros(3, dtype=int)
np.add.at(counts, [0, 0, 2], 1)
print(counts)
$ python add_at.py
[1 0 1]
[2 0 1]

Index 0 appears twice, yet += only counted it once. The statement expands to counts[idx] = counts[idx] + 1, which writes the same value to position 0 twice. np.add.at applies each increment separately, which is what you want for histograms and scatter-adds.

Selecting Samples by Label

import numpy as np

rng = np.random.default_rng(42)
X = rng.standard_normal((1000, 10))
y = rng.integers(0, 5, size=1000)

X3 = X[y == 3]
idx3 = np.flatnonzero(y == 3)
sample = X[rng.choice(idx3, size=50, replace=False)]

print(X3.shape, idx3[:5], sample.shape)
$ python by_label.py
(216, 10) [ 1  2  4 10 18] (50, 10)

X[y == 3] uses a mask built from a different array of the same length, the everyday way to filter features by label. np.flatnonzero returns the positions rather than the values, which you need for sampling indices. With seed 42 roughly a fifth of the 1000 rows are class 3.

Practice

  1. From np.arange(20).reshape(4, 5), take every other column of the last two rows. Is the result a view?
  2. Replace every negative value in np.array([3, -1, 4, -1, 5, -9]) with 0 in place.
  3. Count how many times each value 0–4 appears in [1, 3, 3, 0, 3, 1] using np.add.at.
  4. Fix arr[arr % 2 == 0][0] = 100 so it sets the first even element of arr to 100.
Solution 1
import numpy as np

m = np.arange(20).reshape(4, 5)
part = m[-2:, ::2]
print(part)
print(np.shares_memory(m, part))
$ python practice_slice.py
[[10 12 14]
 [15 17 19]]
True

Slices with steps are still basic slicing, so the result is a view.

Solution 2
import numpy as np

a = np.array([3, -1, 4, -1, 5, -9])
a[a < 0] = 0
print(a)
$ python practice_clip.py
[3 0 4 0 5 0]

A mask on the left-hand side of an assignment writes into the original array.

Solution 3
import numpy as np

counts = np.zeros(5, dtype=int)
np.add.at(counts, [1, 3, 3, 0, 3, 1], 1)
print(counts)
$ python practice_counts.py
[1 2 0 3 0]

Each of the three 3s was counted; counts[[...]] += 1 would have given 1 at index 3.

Solution 4
import numpy as np

arr = np.array([7, 4, 9, 6, 2])
arr[np.flatnonzero(arr % 2 == 0)[0]] = 100
print(arr)
$ python practice_first_even.py
[  7 100   9   6   2]

np.flatnonzero turns the mask into positions, and a single integer index on the left-hand side writes into arr itself.

Frequently Asked Questions

Does NumPy slicing return a copy or a view?
Basic slicing with integers, colons and steps returns a view: a new array object that shares the original's memory, so writing through it changes the original. Fancy indexing with an integer list or a boolean mask returns a copy. np.shares_memory(a, b) tells you for certain which one you have.
What is fancy indexing?
Fancy indexing means indexing with an array of integers or booleans instead of a single integer or slice. It selects arbitrary, non-contiguous elements in one operation, in any order, and always returns a new array that does not share memory with the original.
Why does arr[mask][0] = 5 not change arr?
arr[mask] is fancy indexing, so it produces a temporary copy, and the second index then writes into that copy, which is thrown away. Write through a single indexing operation instead, such as arr[mask] = 5 or arr[np.flatnonzero(mask)[0]] = 5, so the assignment targets the original array.
Why can't I use 'and' or 'or' with NumPy boolean arrays?
Python's and/or ask for the truth value of the whole array, which is ambiguous when it has more than one element, so NumPy raises ValueError. Use the elementwise operators & and | instead, and wrap each comparison in parentheses because & binds more tightly than > or ==.
Why does arr[[0, 0]] += 1 only add 1 once?
With fancy indexing, arr[idx] += 1 is evaluated as arr[idx] = arr[idx] + 1: NumPy reads both copies of element 0, adds one to each, then writes element 0 twice with the same value. To accumulate over repeated indices use np.add.at(arr, idx, 1), which applies every increment.