Topics in this subject
NumPy 2 min read Updated 4 Aug 2026

5. Indexing & Slicing

a = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]

Basic, negative, and slicing#

a = np.arange(10)          # [0 1 2 3 4 5 6 7 8 9]
a[0], a[-1]                # 0, 9
a[2:5]                     # [2 3 4]
a[::2]                     # [0 2 4 6 8]
a[::-1]                    # reversed

Multi-dimensional indexing#

m = np.arange(12).reshape(3, 4)
m[1, 2]        # 6      (row 1, col 2)
m[1]           # [4 5 6 7]  (whole row -> a view)
m[:, 2]        # [2 6 10]   (whole column)
m[0:2, 1:3]    # [[1 2],[5 6]]

📌 Remember: Use m[i, j] not m[i][j]. The comma form is one indexing op; the chained form creates an intermediate array.

Fancy indexing (integer arrays)#

Selecting arbitrary elements with an array of indices → always returns a copy.

a = np.array([10, 20, 30, 40, 50])
a[[0, 2, 4]]        # [10 30 50]
a[[0, 2, 4]] = 0    # assign via fancy index

Boolean indexing (masking)#

a = np.array([1, -2, 3, -4, 5])
a[a > 0]            # [1 3 5]
a[a < 0] = 0        # clamp negatives to 0
np.sum(a > 2)       # count elements > 2  -> 2

🚀 Best Practice: Boolean masking is the vectorized replacement for if-loops. df[df.col > x] in Pandas is the same idea.

Views vs Copies — the critical distinction#

Operation Returns
Basic slicing a[2:5] View (shares memory)
Fancy indexing a[[1,3]] Copy
Boolean indexing a[a>0] Copy
reshape, .T, ravel Usually view
flatten, .copy() Always copy
a = np.arange(5)
s = a[1:4]      # view
s[0] = 99
print(a)        # [ 0 99  2  3  4]  <- parent changed!
flowchart LR
    P["parent a = [0,1,2,3,4]"] --> BUF["shared data buffer"]
    V["view: a[1:4] (basic slice)"] --> BUF
    F["copy: a[[1,3]] (fancy) / .copy()"] --> BUF2["independent buffer"]
    BUF -.->|"mutating the view edits the parent"| P

⚠️ Common Mistake: Mutating a slice mutates the original. If you need independence, call .copy().

Interview Question: Does slicing copy data? Basic slicing returns a view (no copy); fancy/boolean indexing returns a copy.