Indexing & Slicing¶
Three ways to grab elements:
- Basic indexing —
a[2],a[1:5],a[1, 2] - Fancy indexing —
a[[0, 2, 5]]— pass a list of indices - Boolean indexing —
a[a > 5]— pass a True/False mask
1D — basic indexing¶
import numpy as np
a = np.array([10, 20, 30, 40, 50])
print(a[0]) # 10 — first
print(a[-1]) # 50 — last
print(a[1:4]) # [20, 30, 40] — slice
print(a[:3]) # first 3
print(a[::2]) # every 2nd
print(a[::-1]) # reversed
Slicing syntax is the same as Python lists.
2D — arr[row, col]¶
import numpy as np
a = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
])
print(a[0, 0]) # 1 — top-left
print(a[2, 3]) # 12 — bottom-right
print(a[-1, -1]) # 12 — also bottom-right
# Whole row
print(a[1]) # [5, 6, 7, 8]
print(a[1, :]) # same thing
# Whole column
print(a[:, 2]) # [3, 7, 11]
2D slicing — arr[rows, cols]¶
import numpy as np
a = np.array([
[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16],
])
# Top-left 2x2 block
print(a[0:2, 0:2])
# Last 2 rows, last 2 cols
print(a[-2:, -2:])
# Every other row, all cols
print(a[::2, :])
Important — slicing returns a VIEW, not a copy¶
import numpy as np
a = np.array([1, 2, 3, 4, 5])
b = a[1:4] # view!
print(b) # [2 3 4]
b[0] = 999 # modifies a too
print(a) # [1 999 3 4 5] ← changed!
To get an independent copy, call .copy():
import numpy as np
a = np.array([1, 2, 3, 4, 5])
b = a[1:4].copy()
b[0] = 999
print(a) # unchanged: [1 2 3 4 5]
print(b)
Knowing whether you have a view or a copy saves bugs.
2. Fancy indexing — list of indices¶
Pass an array/list of integer indices to pick specific elements:
import numpy as np
a = np.array([10, 20, 30, 40, 50, 60])
print(a[[0, 2, 5]]) # [10 30 60]
print(a[[5, 0, 3]]) # [60 10 40] — order preserved
For 2D — pass two lists, one per axis. Picks specific elements:
import numpy as np
a = np.array([
[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12],
])
# Pick (0,0), (1,1), (2,2), (3,0)
rows = [0, 1, 2, 3]
cols = [0, 1, 2, 0]
print(a[rows, cols]) # [1 5 9 10]
For "all rows of columns 0, 2":
import numpy as np
a = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
])
# Slice + fancy
print(a[:, [0, 2]])
Fancy indexing always returns a COPY (unlike slicing).
3. Boolean indexing — masks¶
The most powerful one. Pass a boolean array of the same shape:
import numpy as np
a = np.array([1, 5, 3, 9, 2, 8, 4])
mask = a > 4
print(mask) # [False, True, False, True, False, True, False]
print(a[mask]) # [5 9 8]
print(a[a > 4]) # same — inline mask
You can build masks with comparison operators (>, <, ==, !=, >=, <=).
Combine masks with & (AND), | (OR), ~ (NOT) — wrap in parens:
import numpy as np
a = np.array([1, 5, 3, 9, 2, 8, 4, 7])
# Between 3 and 8 inclusive
mask = (a >= 3) & (a <= 8)
print(a[mask]) # [5 3 8 4 7]
# Odd numbers
print(a[a % 2 == 1]) # [1 5 3 9 7]
# Not equal to 5
print(a[a != 5])
Don't use and/or — those are for Python booleans, not arrays.
Modify with masks — conditional assignment¶
import numpy as np
a = np.array([1, 5, 3, 9, 2, 8, 4])
# Clip negatives — replace anything < 0 with 0
b = a.copy()
b[b < 0] = 0
print(b)
# Replace anything > 5 with 99
c = a.copy()
c[c > 5] = 99
print(c)
Single-element vs slice — different return types¶
import numpy as np
a = np.arange(10)
x = a[3] # scalar (numpy int)
y = a[3:4] # array of length 1
print(x, type(x).__name__)
print(y, type(y).__name__)
Often surprising — a[3:4] is an array, a[3] is a number.
... (ellipsis) — "all remaining axes"¶
Useful for high-dimensional arrays:
import numpy as np
a = np.ones((2, 3, 4, 5))
print(a[..., 0].shape) # same as a[:, :, :, 0] → (2, 3, 4)
print(a[0, ...].shape) # same as a[0, :, :, :] → (3, 4, 5)
np.newaxis — add a dimension¶
import numpy as np
a = np.array([1, 2, 3, 4])
print(a.shape) # (4,)
col = a[:, np.newaxis]
print(col)
print(col.shape) # (4, 1) — column vector
row = a[np.newaxis, :]
print(row)
print(row.shape) # (1, 4) — row vector
You can also use None: a[:, None] is the same as a[:, np.newaxis].
Putting it together — mini exercise¶
import numpy as np
rng = np.random.default_rng(seed=42)
scores = rng.integers(0, 100, size=15)
print("scores:", scores)
# Top 3 scores
top3 = np.sort(scores)[-3:]
print("top 3:", top3)
# Pass marks (>= 60)
print("passed:", scores[scores >= 60])
# Indices of passes
print("indices of passes:", np.where(scores >= 60)[0])
Common pitfalls¶
- ❗
a[1, 2]vsa[1][2]— both work, buta[1, 2]is more efficient. - ❗ Slice modifications affect the original — slicing returns a view. Use
.copy()if you need an independent array. - ❗ Using
and/oron arrays — Python'sanddoesn't work element-wise. Use&/|and wrap conditions in parens. - ❗ Forgetting parens around boolean conditions —
a > 3 & a < 8is wrong because of operator precedence. Use(a > 3) & (a < 8). - ❗ Fancy indexing returns a COPY — assigning back doesn't always work as expected.
a[[1,2,3]] = 0does set those elements; buta[[1,2,3]] *= 2may not when there are duplicates.
Practice¶
What does this print?
Expected: [20 30 40]
Get values between 3 and 8 inclusive (you should get [3 5 8 4 7])
Expected: [3 5 8 4 7]
Quiz — Quick check¶
What you remember
Q1. Slicing a NumPy array returns a…
- Deep copy
- View — a window into the same underlying data
- List
- New array always
Why: Slicing a NumPy array gives you a view. Modifying it modifies the original. Use
.copy()if you need an independent array.
Q2. Why must boolean conditions use & and |, not and and or?
-
andis slower -
and/oronly work on single booleans;&/|are element-wise -
&is deprecated -
andreturns the wrong type
Why:
a > 3 and a < 8would try to evaluate a whole array as one boolean, raisingValueError: ambiguous truth value.(a > 3) & (a < 8)produces an element-wise mask — what you want.
Q3. For a = np.arange(10), which gives a scalar vs array?
-
a[3]→ array;a[3:4]→ scalar -
a[3]→ scalar;a[3:4]→ array of length 1 - Both return arrays
- Both return scalars
Why: Integer indexing drops a dimension (scalar from 1D). Slice indexing preserves the dimension (array, even of length 1).
Common doubts¶
How do I know if I have a view or a copy?
Check with b.base is a — if True, b is a view of a. Rule of thumb: basic slicing returns a view; fancy indexing (lists of indices) and boolean masking return copies. When in doubt, use .copy() to guarantee independence.
Why do I need parens around (a > 3) & (a < 8)?
Python's & operator has higher precedence than < / >. Without parens, a > 3 & a < 8 is parsed as a > (3 & a) < 8 — totally wrong. Always wrap boolean conditions in parens when combining with & / |.
What's the difference between a[1, 2] and a[1][2]?
Both give the same element for 2D arrays, but a[1, 2] is faster — it's a single indexing operation. a[1][2] first creates an intermediate view of row 1, then indexes that. For high-dimensional arrays, comma-separated indexing is the idiom.