NumPy arrays support all of Python’s list-style indexing and slicing, plus a few extras designed for multi-dimensional data.
Basic indexing — 1D
Same as lists:
import numpy as np
a = np.arange(10)
print(a) # [0 1 2 3 4 5 6 7 8 9]
print(a[0]) # 0
print(a[-1]) # 9
print(a[2:5]) # [2 3 4]
print(a[:3]) # [0 1 2]
print(a[::2]) # [0 2 4 6 8] — every other
print(a[::-1]) # [9 8 7 6 5 4 3 2 1 0] — reversed
Everything you learned for Python lists works on NumPy 1D arrays.
Indexing 2D arrays
For a matrix, you use two indices separated by a comma:
m = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
])
print(m[0, 0]) # 1 — first row, first column
print(m[1, 2]) # 7 — second row, third column
print(m[-1, -1]) # 12 — last row, last column
This is the NumPy way. The old list-style m[0][0] works but is slower.
Slicing 2D arrays
Slice each dimension independently:
print(m[0]) # [1 2 3 4] — first row (whole)
print(m[:, 0]) # [1 5 9] — first column
print(m[1:, :2]) # [[ 5 6]
# [ 9 10]]
print(m[:, ::2]) # [[ 1 3]
# [ 5 7]
# [ 9 11]]
The colon : alone means “all of this dimension”. m[:, 0] reads as “all rows, column 0”.
Fancy indexing — picking by list of indices
Instead of a slice, you can pass a list of indices:
a = np.array([10, 20, 30, 40, 50])
indices = [0, 2, 4]
print(a[indices]) # [10 30 50]
You can also pass an array of indices:
print(a[np.array([4, 2, 0])]) # [50 30 10] — in that order
This is very useful for picking specific elements by their position — common in ML when you want certain training examples or features.
Boolean indexing (a recap)
We met this in lesson 2. A boolean mask of the same shape picks the True positions:
a = np.array([1, 2, 3, 4, 5])
print(a[a > 2]) # [3 4 5]
# common pattern: replace bad values
a[a < 0] = 0 # set every negative element to 0
Boolean indexing for 2D:
m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(m[m > 5]) # [6 7 8 9] — flattened
Note that boolean indexing on 2D returns a 1D array, regardless of the mask’s shape.
Modifying with indexing
All the indexing forms work for assignment too:
a = np.zeros(10)
a[0] = 1
a[3:6] = [10, 20, 30]
a[a > 5] = 99
print(a)
# [ 1. 0. 0. 99. 99. 99. 0. 0. 0. 0.]
You can change many positions in one statement. Try writing the equivalent with a for loop — it’s three times longer.
A subtle gotcha — views vs copies
A slice of a NumPy array is not a copy. It’s a view of the same underlying data:
a = np.array([1, 2, 3, 4, 5])
b = a[1:4] # view, not a copy
b[0] = 99
print(a) # [ 1 99 3 4 5] — a changed!
This is a feature (zero-copy slicing is fast) but a surprise if you’re used to Python lists, which copy on slice.
To get an independent copy, use .copy():
b = a[1:4].copy()
b[0] = 99
print(a) # unchanged
For fancy indexing and boolean indexing, NumPy does copy — so the gotcha only applies to basic slicing.
A practical example — extract a sub-region of an image
An image is a 3D array of shape (height, width, 3) for RGB:
# imagine `image` is shape (480, 640, 3)
# crop the top-left 100x100 region
top_left = image[:100, :100, :]
print(top_left.shape) # (100, 100, 3)
# just the green channel
green_only = image[:, :, 1]
print(green_only.shape) # (480, 640)
Slicing across multiple dimensions is the kind of thing that takes a paragraph in plain Python and one line in NumPy.
What’s next
You can pick any subset of any array. Last lesson — vectorisation, the philosophy behind why NumPy code looks the way it does.