Skip to main content
NumPy beginner Lesson 4 of 12

NumPy Indexing and Slicing

Select, filter, and update array elements using integer indexing, slices, boolean masks, and fancy indexing.

Real-World Scenario

An ML engineer working with a dataset of 50,000 images needs to extract specific pixel regions from every image, select all samples where the label equals 3, and replace corrupted values above a threshold. These are indexing and masking operations — the core data access patterns you’ll use in every NumPy workflow.

Basic Indexing

import numpy as np

arr = np.array([10, 20, 30, 40, 50])

# Single element — 0-based, like Python lists
print(arr[0])    # 10
print(arr[-1])   # 50 — negative indices count from the end
print(arr[-2])   # 40

# 2-D indexing — [row, col]
matrix = np.array([
    [1,  2,  3,  4],
    [5,  6,  7,  8],
    [9, 10, 11, 12]
])

print(matrix[0, 0])   # 1  — top-left
print(matrix[2, 3])   # 12 — bottom-right
print(matrix[-1, -1]) # 12 — same element, negative indices

# Selecting an entire row or column
print(matrix[1])       # [5 6 7 8] — row 1
print(matrix[:, 2])    # [ 3  7 11] — column 2 (all rows, col index 2)

Slicing

NumPy slice syntax is start:stop:step, identical to Python list slicing but extended to multiple dimensions. Slices return views — not copies.

import numpy as np

arr = np.array([0, 10, 20, 30, 40, 50, 60, 70, 80, 90])

print(arr[2:6])      # [20 30 40 50] — elements at index 2, 3, 4, 5
print(arr[:4])       # [ 0 10 20 30] — from start to index 3
print(arr[6:])       # [60 70 80 90] — from index 6 to end
print(arr[::2])      # [ 0 20 40 60 80] — every other element
print(arr[::-1])     # [90 80 70 ... 0] — reversed

# 2-D slicing — extract a submatrix
matrix = np.arange(16).reshape(4, 4)
print(matrix)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]
#  [12 13 14 15]]

# Rows 1–2, columns 1–3
sub = matrix[1:3, 1:3]
print(sub)
# [[ 5  6]
#  [ 9 10]]

# Views share memory — modifying sub modifies matrix
sub[0, 0] = 99
print(matrix[1, 1])  # 99

# Make an independent copy to avoid this
safe_sub = matrix[1:3, 1:3].copy()

Boolean Masking

Boolean masking is the most readable way to filter arrays. Comparison operators applied to an array return a boolean array of the same shape. Passing that boolean array as an index selects only the True elements.

import numpy as np

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

# Step 1: comparison produces a boolean array
hot_mask = temperatures > 30
print(hot_mask)  # [False  True False  True False False  True]

# Step 2: use the mask as an index — returns a 1-D array of matching values
hot_temps = temperatures[hot_mask]
print(hot_temps)  # [35.4 41.2 38.1]

# Combine in one line
print(temperatures[temperatures > 30])

# Multiple conditions — use & (and) and | (or), not Python's 'and'/'or'
comfortable = temperatures[(temperatures >= 20) & (temperatures <= 30)]
print(comfortable)  # [22.1 28.7]

# np.where — like a vectorized ternary: where(condition, if_true, if_false)
labels = np.where(temperatures > 30, "hot", "normal")
print(labels)  # ['normal' 'hot' 'normal' 'hot' 'normal' 'normal' 'hot']

# Replace values in-place using a mask
temperatures[temperatures > 40] = 40.0  # clamp outliers
print(temperatures)  # max is now 40.0

Fancy Indexing

Use an array of integer indices to select non-contiguous elements in arbitrary order. This always returns a copy.

import numpy as np

prices = np.array([9.99, 14.99, 4.99, 24.99, 7.49, 19.99])

# Select specific elements by position
selected = prices[[0, 2, 5]]
print(selected)  # [ 9.99  4.99 19.99]

# Reorder elements
shuffled = prices[[3, 0, 4, 1]]
print(shuffled)  # [24.99  9.99  7.49 14.99]

# 2-D fancy indexing — select specific rows
data = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
    [10, 11, 12]
])

# Select rows 0 and 2
print(data[[0, 2]])
# [[1 2 3]
#  [7 8 9]]

# Select specific (row, col) pairs — extracts diagonal-style elements
rows = np.array([0, 1, 2])
cols = np.array([2, 0, 1])
print(data[rows, cols])  # [3 4 8] — data[0,2], data[1,0], data[2,1]

Real-World: Selecting Training Samples by Label

import numpy as np

rng = np.random.default_rng(42)

# Simulate a dataset: 1000 samples, 10 features each
X = rng.standard_normal((1000, 10))
y = rng.integers(0, 5, size=1000)  # labels 0–4

# Select all samples belonging to class 3
class_3_mask = y == 3
X_class3 = X[class_3_mask]
print(f"Class 3 samples: {X_class3.shape[0]}")  # ~200

# Get indices where label == 3 (useful when you need positions, not values)
class_3_indices = np.where(y == 3)[0]
print(class_3_indices[:5])  # [4 7 9 ...]

# Sample 50 random class-3 examples
sample_idx = rng.choice(class_3_indices, size=50, replace=False)
X_sample = X[sample_idx]
print(X_sample.shape)  # (50, 10)

Common Mistakes

1. Modifying a slice when you need a copy:

arr = np.array([1, 2, 3, 4, 5])
view = arr[1:4]
view[0] = 99  # also modifies arr[1] — unexpected if you forgot view is a view

2. Using Python and/or on boolean arrays:

# Wrong — raises ValueError
# mask = (arr > 2) and (arr < 5)

# Correct — element-wise bitwise operators
mask = (arr > 2) & (arr < 5)

3. Off-by-one in stop index:

arr = np.arange(10)
print(arr[2:5])  # [2 3 4] — stop=5 is excluded, like Python range

Frequently Asked Questions

Does NumPy slicing return a copy or a view?
Basic slicing (colons and integers) returns a view — a window into the same memory. Modifying the view modifies the original. Fancy indexing (integer arrays or boolean masks) returns a copy. Call .copy() explicitly when you need an independent array from a slice.
What is fancy indexing?
Fancy indexing means using an array of integers or booleans as the index instead of a single integer or slice. It lets you select non-contiguous elements in one operation. It always returns a copy, not a view.