On this page
NumPy indexing and slicing pulls data out of arrays with b[row, col], ranges like a[1:4], boolean masks like a[a > 25], and lists of positions. It looks like list indexing, with one big difference: a slice is a view onto the same memory, so writing to it changes the original array.
That rule catches people who clean a slice of a dataset and later find the raw data altered. Masks and fancy indexing do the opposite and return copies. This part of the series is for readers who can create arrays, as covered in Part 1. It shows each kind of indexing with real output, how to tell a view from a copy, and the mistakes behind most indexing bugs.
Every output below comes from running the code on NumPy 2.2. The rules themselves are documented in the official NumPy indexing guide.
How does basic indexing work in NumPy?
One dimension behaves like a list:
import numpy as np
a = np.array([10, 20, 30, 40, 50])
a[0] # 10
a[-1] # 50
a[1:4] # [20 30 40]
a[::2] # [10 30 50]
a[::-1] # [50 40 30 20 10]
Multiple dimensions, however, use a comma rather than chained brackets:
b = np.arange(12).reshape(3, 4)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
b[1, 2] # 6 - row 1, column 2
b[1] # [4 5 6 7] - whole row
b[:, 1] # [1 5 9] - whole column
b[0:2, 1:3] # [[1 2]
# [5 6]]
b[1][2] also works, but it first creates a temporary array for b[1] and then indexes that. As a result, b[1, 2] is the idiomatic and faster form.
In a notebook, a single element displays as np.int64(6) rather than 6. That is the NumPy 2 scalar representation; the value is the same.
Why does changing a slice change the original array?
Because basic slicing returns a view that shares memory with the source array, not a copy. This is the single most important thing in this article.
original = np.array([1, 2, 3, 4, 5])
piece = original[1:4]
piece[0] = 999
print(original) # [ 1 999 3 4 5] <- the source changed
Basic slicing returns a view: a window onto the same memory. Nothing is copied, which is why slicing a million-element array is instant.
That is a feature, until you slice a dataset, clean the slice, and then discover you have modified the raw data you meant to preserve.
Check with .base:
piece.base is original # True - it is a view
original.base is None # True - it owns its data
Take an explicit copy when you need independence:
original = np.array([1, 2, 3, 4, 5])
safe = original[1:4].copy()
safe[0] = 999
print(original) # [1 2 3 4 5] - untouched this time
The rule: if you are going to write to it, copy it. The copies and views page in the NumPy docs lists which other operations, such as reshape and ravel, return views where possible.
How do you filter a NumPy array with a condition?
Use boolean masking, the idiomatic way to filter. A comparison produces a boolean array, and indexing with that array selects the positions where it is True.
temps = np.array([18, 25, 31, 12, 28, 35])
mask = temps > 25
print(mask) # [False False True False True True]
print(temps[mask]) # [31 28 35]
# usually written inline
print(temps[temps > 25])
Combine conditions with &, | and ~, not and, or and not:
temps[(temps > 20) & (temps < 32)] # [25 31 28]
temps[(temps < 15) | (temps > 30)] # [31 12 35]
temps[~(temps > 25)] # [18 25 12]
Notice that the result keeps the original order: 31 comes before 12 because it appears earlier in temps.
The parentheses are required. Because & binds more tightly than >, the expression temps > 20 & temps < 32 parses as something quite different and raises an error.
Python's and fails here for a related reason. It needs a single boolean from an array of many, which NumPy refuses to guess, hence the familiar "truth value of an array with more than one element is ambiguous".
Masks also work on the left of an assignment:
scores = np.array([45, 82, 91, 38, 67])
scores[scores < 50] = 0
print(scores) # [ 0 82 91 0 67]
Meanwhile, np.where gives a vectorised if/else:
np.where(scores >= 50, 'pass', 'fail')
# ['fail' 'pass' 'pass' 'fail' 'pass']
What is fancy indexing in NumPy?
Fancy indexing means indexing with a list or array of positions. It selects elements in any order, with repeats, and always returns a copy:
a = np.array([10, 20, 30, 40, 50])
a[[0, 3, 4]] # [10 40 50]
a[[2, 2, 0]] # [30 30 10] - repeats allowed
In two dimensions, paired index arrays select individual elements:
b = np.arange(12).reshape(3, 4)
rows = [0, 1, 2]
cols = [1, 2, 3]
b[rows, cols] # [ 1 6 11] - (0,1), (1,2), (2,3)
Fancy indexing and boolean masking both return copies, not views. The selected elements are not generally evenly spaced, so there is nothing to view. That is the reverse of the slicing rule, and it is worth remembering.
f = a[[0, 1]]
np.shares_memory(f, a) # False - a copy
np.shares_memory(a[1:3], a) # True - a view
How do you change part of a NumPy array in place?
Assign to a slice. The value on the right is broadcast across the selected positions:
b = np.zeros((3, 4))
b[1, :] = 5 # whole row becomes 5
b[:, 0] = [1, 2, 3] # column gets these values
b[0:2, 0:2] = 9 # block becomes 9
This is efficient, since it writes in place rather than building a new array. Part 3 explains the broadcasting rules that decide which shapes are allowed on the right-hand side.
Worked example: filtering rows of a table
Filtering rows of a 2D array by a condition on one column:
data = np.array([
[1, 25, 50000],
[2, 32, 72000],
[3, 19, 31000],
[4, 45, 95000],
])
# rows where age (column 1) is over 30
adults = data[data[:, 1] > 30]
# [[ 2 32 72000]
# [ 4 45 95000]]
data[:, 1] > 30 produces a mask of length 4, one value per row. Using it as the row index then selects the matching rows whole. Because this is boolean masking, adults is a copy, so you can edit it without touching data.
NumPy indexing and slicing quick reference
| Syntax | Selects | Returns |
|---|---|---|
a[2], b[1, 2] |
one element | a scalar |
a[1:4], b[:, 1] |
a range or row/column | a view |
a[::-1] |
reversed range | a view |
a[a > 25] |
elements matching a mask | a copy |
a[[0, 3, 4]] |
listed positions | a copy |
np.where(cond, x, y) |
elementwise if/else | a new array |
a[1:4].copy() |
a range, detached | a copy |
Common NumPy indexing and slicing mistakes
- Editing a slice you meant to keep separate. Slices are views, so call
.copy()first. - Using
and/orwith arrays. Use&,|and~, with parentheses around each comparison. - Chaining fancy indexes on the left of
=.d[[0, 2]][0] = 99edits a temporary copy and leavesdunchanged; writed[0] = 99instead. - Writing
b[1][2]in hot loops. It builds a temporary array;b[1, 2]does not. - Assuming out-of-range slices raise errors.
a[2:100]quietly returns[30 40 50], whereasa[10]raisesIndexError.
The Sunday Growth Brief
One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.
No spam. Unsubscribe in one click.
Related reading
NumPy Part 1: arrays covers the basics. NumPy Part 3: broadcasting continues the series, and Part 4 finishes it with aggregation and linear algebra. Python's own slicing rules for sequences are covered in lists vs tuples in Python.
Frequently asked questions
Why did modifying a slice change my original array?
Basic slicing returns a view that shares the same memory buffer as the original array, not a copy. This is deliberate, because it makes slicing almost free even on very large arrays. The side effect is that any assignment through the slice writes into the source. Call .copy() on the slice whenever you need an independent array to modify.
How do I check whether a NumPy array is a view?
Look at the .base attribute. It is None for an array that owns its data, and it points at the source array for a view, so arr_slice.base is original returns True. For a more general test between any two arrays, np.shares_memory(a, b) tells you whether they overlap in memory, which also catches views of views.
Why does mask1 and mask2 raise an error in NumPy?
Python's and keyword needs a single True or False for the whole array, and NumPy refuses to guess, so it raises the familiar error that the truth value of an array is ambiguous. Use the elementwise operators &, | and ~ instead, and wrap each comparison in parentheses because those operators bind more tightly than > and <.
What is the difference between fancy indexing and slicing?
Slicing uses start:stop:step ranges and returns a view into the same memory. Fancy indexing uses an array or list of positions, or a boolean mask, and always returns a copy, because the selected elements are not generally evenly spaced in memory. Both can be used on the left of an assignment to change the original array in place.
Written by
Vanshika Nigam
ETL & Data Engineer, Accenture
ETL developer at Accenture working with Informatica, Snowflake and DBT, specialising in data mapping, cleansing and pipeline performance.

