NumPy Array Indexing and Slicing

NumPy array indexing and slicing work differently than Python lists in ways that matter. While both support basic integer indexing and slicing, NumPy gives you comma indexing for multidimensional access, views that reference the original data (not copies), boolean masks to filter values, and fancy indexing with integer arrays. If you’re new to NumPy arrays, start with the basics first. Understanding how indexing and slicing differ from Python is the foundation for efficient NumPy work.

Basic 1D Indexing

Indexing a one-dimensional NumPy array looks similar to indexing a Python list, but NumPy adds negative indexing from the start.

import numpy as np

arr = np.arange(0, 11)
print(arr)

Output:

[ 0  1  2  3  4  5  6  7  8  9 10]

Access individual elements by position:

print("Element at index 7:", arr[7])
print("Last element (index -1):", arr[-1])
print("Third from last (index -3):", arr[-3])

Output:

Element at index 7: 7
Last element (index -1): 10
Third from last (index -3): 8

Negative indices count from the end, starting at -1 for the last element. This works for Python lists too, so it’s not a difference. The real differences start with multidimensional access.

Multidimensional Indexing with Commas

NumPy arrays let you index multiple dimensions with commas, a feature Python lists don’t have. No nested brackets needed:

arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2d)

Output:

[[1 2 3]
 [4 5 6]]

Access elements using array[row, column]:

print("Element at [1, 2]:", arr2d[1, 2])
print("Element at [0, -1]:", arr2d[0, -1])

Output:

Element at [1, 2]: 6
Element at [0, -1]: 3

You can use negative indices on any dimension. With Python lists of lists, you’d have to nest brackets: list_2d[1][2], which is clunkier and doesn’t support negative indices on the second level.

Slicing One-Dimensional Arrays

Slicing uses the syntax array[start:stop:step]. The stop index is exclusive, and the step is optional (defaults to 1).

arr = np.array([10, 11, 12, 13, 14])
print("Original:", arr)
print("Slice [1:4]:", arr[1:4])

Output:

Original: [10 11 12 13 14]
Slice [1:4]: [11 12 13]

Like Python lists, the slice includes start and excludes stop. You can omit start or stop to slice from the beginning or to the end.

Slicing Two-Dimensional Arrays

With 2D arrays, slicing both dimensions uses the syntax array[row_slice, col_slice]:

arr2d = np.array([[10, 11, 12], [13, 14, 15], [16, 17, 18]])
print(arr2d)
print("\nSlice [0:2, 0:2]:")
print(arr2d[0:2, 0:2])
print("\nSlice [1:3, 1:3]:")
print(arr2d[1:3, 1:3])

Output:

[[10 11 12]
 [13 14 15]
 [16 17 18]]

Slice [0:2, 0:2]:
[[10 11]
 [13 14]]

Slice [1:3, 1:3]:
[[14 15]
 [17 18]]

This is where NumPy diverges sharply from Python. A list of lists can’t slice columns. You’d have to rebuild the structure manually.

2D NumPy array slicing visualization A 3x3 grid showing array elements with row and column indices, highlighting the slice [1:3, 1:3] with outlined cells. Rows Columns 0 1 2 0 1 2 10 11 12 13 14 15 16 17 18 arr[0:1, :] arr[1:3, 1:3] (highlighted)
A 3x3 NumPy array with the slice [1:3, 1:3] highlighted, selecting rows 1-2 and columns 1-2 to produce [[14, 15], [17, 18]].

Slicing Returns a View, Not a Copy

Here’s a gotcha: NumPy slices return a view of the original data, not a copy. Change the slice and you change the original array.

arr = np.arange(10)
print("Original array:", arr)

arr_slice = arr[2:7]
print("Slice [2:7]:", arr_slice)

arr_slice[0] = 999
print("After changing slice[0] to 999:")
print("Original array:", arr)

Output:

Original array: [0 1 2 3 4 5 6 7 8 9]
Slice [2:7]: [2 3 4 5 6]
After changing slice[0] to 999:
Original array: [  0   1 999   3   4   5   6   7   8   9]

Python list slices are independent copies, so modifying a slice doesn’t touch the original. If you need a copy in NumPy, call .copy() explicitly.

Boolean Indexing (Masking)

Use a boolean array to filter values by condition:

arr = np.array([1., -1., -2., 3., -4.])
print("Original array:", arr)
print("Elements < 0:", arr[arr < 0])

arr[arr < 0] = 0
print("After setting negative values to 0:", arr)

Output:

Original array: [ 1. -1. -2.  3. -4.]
Elements < 0: [-1. -2. -4.]
After setting negative values to 0: [1. 0. 0. 3. 0.]

The expression arr < 0 creates a boolean array, and arr[boolean_array] returns only elements where the boolean is True. You can assign back to the same selection to modify all matching values at once. Python lists have no direct equivalent. You’d need a list comprehension.

Fancy Indexing with Integer Arrays

Pass an integer array as an index to select arbitrary elements in order:

arr = np.arange(10, 1, -1)
print("Original array:", arr)

indices = np.array([3, 3, 1, 8])
print("Values at indices [3, 3, 1, 8]:", arr[indices])

Output:

Original array: [10  9  8  7  6  5  4  3  2]
Values at indices [3, 3, 1, 8]: [7 7 9 2]

Notice you can repeat indices and get the same element multiple times. Unlike basic slicing, fancy indexing returns a copy, not a view.

For 2D arrays, you can select arbitrary rows and columns:

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

rows = np.array([0, 3])
cols = np.array([0, 2])
print("Corner elements [rows=[0,3], cols=[0,2]]:")
print(arr2d[rows[:, np.newaxis], cols])

Output:

[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]

Corner elements [rows=[0,3], cols=[0,2]]:
[[ 0  2]
 [ 9 11]]

The [:, np.newaxis] adds a dimension so NumPy broadcasts the row and column indices correctly.

Practical Use Cases

These indexing techniques combine in real data work. Boolean indexing filters noisy data, fancy indexing reorders or deduplicates results, and views save memory on large arrays. Slicing with commas avoids nested loops. Together they make NumPy fast and expressive for numerical work.

For the full details and advanced indexing modes, see the NumPy indexing documentation.

Have you hit a case where NumPy indexing behaved differently than you expected?