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.

Toggle theme (T)