Skip to main content

Indexing, Slicing and Reshaping

Topic — Getting at parts of an array, and changing its shape. The one thing that genuinely differs from Python lists is in the middle of this page: slices are views.


1-D indexing and slicing

Identical to strings and lists0 first, -1 last, stop excluded.

arr = np.array([10, 20, 30, 40, 50])
print(arr[0]) # → 10
print(arr[2]) # → 30
print(arr[-1]) # → 50
print(arr[1:4]) # → [20 30 40]
print(arr[:3]) # → [10 20 30]
print(arr[2:]) # → [30 40 50]
print(arr[::2]) # → [10 30 50]
print(arr[::-1]) # → [50 40 30 20 10]
Output
10
30
50
[20 30 40]
[10 20 30]
[30 40 50]
[10 30 50]
[50 40 30 20 10]

Nothing new so far. That's the point — the syntax carries over.


2-D indexing

Here NumPy adds something lists don't have: one bracket, comma-separated per dimension.

a = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])

print(a[1, 2]) # → 6 row 1, column 2
print(a[1][2]) # → 6 same answer, worse way
Output
6
6

a[1, 2] reads "row 1, column 2" in one operation. a[1][2] builds an intermediate array for row 1, then indexes into it — same answer, more work. Prefer the comma form.

The mental model:

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

Rows and columns

print(a[0])        # → [1 2 3]   whole row
print(a[:, 0]) # → [1 4 7] whole column
Output
[1 2 3]
[1 4 7]

a[0] is row 0. For a column you need a[:, 0] — "all rows, column 0". Extracting a column from a list of lists takes a comprehension; here it's punctuation.

2-D slicing

Slice each dimension independently:

print(a[1, 2])        # → 6                single element
print(a[:2, :2]) # first 2 rows, first 2 columns
print(a[1:, :2]) # from row 1, first 2 columns
print(a[:, 1]) # → [2 5 8] column 1
print(a[::2, ::2]) # every other row and column
Output
6
[[1 2]
[4 5]]
[[4 5]
[7 8]]
[2 5 8]
[[1 3]
[7 9]]
Indexing a dimension drops it; slicing keeps it
print(a[:, 1].shape)      # → (3,)    1-D — the column dimension is gone
print(a[:, 1:2].shape) # → (3, 1) still 2-D — a one-column matrix

a[:, 1] uses an index for the column, so that dimension collapses. a[:, 1:2] uses a slice, so it survives with length 1. This bites when a function insists on 2-D input.


Slices are views, not copies

This is the most important difference between NumPy and everything else you've seen.

s = np.array([1, 2, 3, 4, 5])
sl = s[1:3]
sl[0] = 99
print(s) # → [ 1 99 3 4 5]
Output
[ 1 99  3  4  5]

Writing to the slice changed the original. Compare a Python list, which copies:

L = [1, 2, 3, 4, 5]
Ls = L[1:3]
Ls[0] = 99
print(L) # → [1, 2, 3, 4, 5] untouched
Output
[1, 2, 3, 4, 5]
This is the reverse of list behaviour

On the Lists page, lst[:] is the idiomatic way to copy a list. On a NumPy array, arr[:] is a view into the same memory — the opposite. Carrying the list habit over to NumPy produces silent data corruption.

A slice is deliberately a view: NumPy arrays are often large, and copying a million-row slice just to read it would be wasteful. The trade is that writes propagate.

Checking, and opting out

sl = s[1:3]
print(sl.base is not None) # → True this array borrows someone else's memory

c = s[1:3].copy()
c[0] = 777
print(s) # → unchanged by the write to c
Output
True

.base is None for an array that owns its data, and points at the original for a view. Call .copy() whenever you intend to modify a slice independently.

OperationReturns
arr[1:3]view
arr[:, 0]view
arr.reshape(...)view
arr.ravel()view (usually)
arr.Tview
arr.copy()copy
arr.flatten()copy
arr[[0, 2]] (fancy)copy
arr[arr > 5] (boolean)copy

The pattern: slicing and reshaping give views; selecting arbitrary elements gives copies. A view has to be describable as "start here, step by this much", and an arbitrary selection isn't.


Boolean masking

Comparing an array gives an array of booleans, which you can then use as an index.

a = np.array([10, 20, 30, 40])
print(a > 20) # → [False False True True] the mask
print(a[a > 20]) # → [30 40] the selection
Output
[False False  True  True]
[30 40]

Read a[a > 20] as "the elements of a where a > 20". This replaces a filtering loop entirely.

Combining conditions

print(a[(a > 15) & (a < 40)])   # → [20 30]
print(a[~(a > 20)]) # → [10 20]
print((a > 20).sum()) # → 2
Output
[20 30]
[10 20]
2
UseNot
and&and
or|or
not~not
warning
and / or / not don't work on arrays

They ask for one True/False, and an array of many values can't answer. You get ValueError: The truth value of an array with more than one element is ambiguous. Use &, |, ~ — and parenthesise each condition, because & binds tighter than >.

(a > 20).sum() counts matches, because True is 1 — the same boolean-as-int fact from the basics.

np.where

Two jobs, depending on argument count.

a = np.array([10, 20, 30, 40])
print(np.where(a > 20, a, 0)) # → [ 0 0 30 40] choose per element
print(np.where(a > 20)) # → (array([2, 3]),) the indices
print(a[np.where(a > 20)]) # → [30 40]
Output
[ 0  0 30 40]
(array([2, 3]),)
[30 40]

Three arguments is a vectorised if/else — "where the condition holds take a, otherwise take 0". One argument returns the positions where it holds, as a tuple of index arrays (one per dimension, hence the trailing comma).

Fancy indexing

Index with a list of positions, in any order, with repeats:

a = np.array([10, 20, 30, 40, 50])
print(a[[0, 2, 4]]) # → [10 30 50]
print(a[[4, 4, 0]]) # → [50 50 10]
Output
[10 30 50]
[50 50 10]

The result is a copy, and its length is the length of your index list — not the array's.


Reshaping

reshape reorganises the same elements into a new shape.

b = np.arange(1, 7)
print(b) # → [1 2 3 4 5 6]
print(b.reshape(2, 3))
print(b.reshape(3, 2))
print(b.reshape(6, 1))
Output
[1 2 3 4 5 6]
[[1 2 3]
[4 5 6]]
[[1 2]
[3 4]
[5 6]]
[[1]
[2]
[3]
[4]
[5]
[6]]

Elements fill row by row.

The legality rule

b.reshape(4, 2)
Error
ValueError: cannot reshape array of size 6 into shape (4,2)

The product of the new shape must equal .size. Six elements reshape to (2,3), (3,2), (6,1), (1,6) — because each multiplies to 6. (4,2) needs 8 elements, so there's nothing to put in the last two slots and NumPy refuses rather than inventing values.

Check with arr.size before reshaping, or let NumPy do the arithmetic:

print(b.reshape(-1, 2))
Output
[[1 2]
[3 4]
[5 6]]

-1 means "work this dimension out from the others". Only one -1 is allowed.

Reshape returns a view

r = b.reshape(2, 3)
r[0, 0] = 99
print(b) # → [99 2 3 4 5 6]
Output
[99  2  3  4  5  6]

Same trap as slicing — reshape doesn't copy. If you need an independent result, chain .copy().

Flattening

Both collapse to 1-D; the difference is the view/copy question again.

m = np.array([[1, 2], [3, 4]])

f = m.flatten()
f[0] = 99
print(m.tolist()) # → [[1, 2], [3, 4]] flatten COPIES

rv = m.ravel()
rv[0] = 99
print(m.tolist()) # → [[99, 2], [3, 4]] ravel VIEWS
Output
[[1, 2], [3, 4]]
[[99, 2], [3, 4]]

flatten() is the safe one. ravel() is faster because it avoids the copy when it can.

Transpose

.T swaps the axes — rows become columns.

m = np.array([[1, 2, 3],
[4, 5, 6]])
print(m.shape, "->", m.T.shape) # → (2, 3) -> (3, 2)
print(m.T)
Output
(2, 3) -> (3, 2)
[[1 4]
[2 5]
[3 6]]

Also a view — and note it's .T, an attribute, with no parentheses.


Common Mistakes

#MistakeWrongRight
1Assuming a slice copiesarr[1:3] shares memoryarr[1:3].copy()
2arr[:] to copyit's a viewarr.copy()
3Assuming reshape copiesit's a viewarr.reshape(2,3).copy()
4Chained 2-D indexinga[1][2]a[1, 2]
5and in a maskValueError: truth value ... ambiguous&
6Unparenthesised maska > 15 & a < 40(a > 15) & (a < 40)
7Reshape to a wrong sizesize 6 into shape (4,2)product must equal .size
8.T() with parenthesesTypeError.T

Summary

TaskSyntax
Element (2-D)a[row, col]
Whole row / columna[i] / a[:, j]
Sub-blocka[r1:r2, c1:c2]
Filter by conditiona[a > x]
Vectorised if/elsenp.where(cond, x, y)
Positions matchingnp.where(cond)
Pick specific indicesa[[i, j, k]]
Change shape.reshape(r, c) or .reshape(-1, c)
Collapse to 1-D.flatten() (copy) / .ravel() (view)
Swap axes.T
Force a copy.copy()

Key takeaways

  • 1-D indexing and slicing match lists and strings exactly
  • 2-D uses one bracket with a comma: a[1, 2], and a[:, 0] for a column
  • Indexing a dimension drops it; slicing keeps it — (3,) vs (3, 1)
  • Slices, reshapes, ravel and .T are views — writing to them writes to the original
  • Boolean masks and fancy indexing return copies
  • Use &, |, ~ in masks, never and/or/not, and parenthesise each condition
  • reshape is legal only when the new shape's product equals .size; -1 infers one dimension
  • .copy() is the escape hatch whenever you plan to modify

See also: Arrays and Attributes · Operations and Broadcasting · Strings and Slicing for the syntax this builds on


Run It Yourself

numpy_indexing.py
import numpy as np

a = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])

print(a)
print(f"{'a[1, 2]':<14} {a[1, 2]}")
print(f"{'row a[0]':<14} {a[0]}")
print(f"{'col a[:, 0]':<14} {a[:, 0]}")
print(f"{'a[:2, :2]':<14} {a[:2, :2].tolist()}")
print(f"{'a[::2, ::2]':<14} {a[::2, ::2].tolist()}")

# masking
flat = a.ravel()
print(f"{'flat':<14} {flat}")
print(f"{'mask > 5':<14} {flat > 5}")
print(f"{'flat[> 5]':<14} {flat[flat > 5]}")
print(f"{'count > 5':<14} {(flat > 5).sum()}")
print(f"{'where':<14} {np.where(flat > 5, flat, 0)}")

# the view trap, demonstrated
original = np.array([1, 2, 3, 4, 5])
view = original[1:3]
view[0] = 99
print(f"{'after view':<14} {original}")

original = np.array([1, 2, 3, 4, 5])
copy = original[1:3].copy()
copy[0] = 99
print(f"{'after copy':<14} {original}")
Output
[[1 2 3]
[4 5 6]
[7 8 9]]
a[1, 2] 6
row a[0] [1 2 3]
col a[:, 0] [1 4 7]
a[:2, :2] [[1, 2], [4, 5]]
a[::2, ::2] [[1, 3], [7, 9]]
flat [1 2 3 4 5 6 7 8 9]
mask > 5 [False False False False False True True True True]
flat[> 5] [6 7 8 9]
count > 5 4
where [0 0 0 0 0 6 7 8 9]
after view [ 1 99 3 4 5]
after copy [1 2 3 4 5]

Practice Questions

From the Unit 1 question bank. Tags and marks are explained on the Python index.

Unit 1 § J — NumPy

J5. [OUT] 2-D slicing. [5]

import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])

print(a[1, 2])
print(a[:2, :2])
print(a[1:, :2])
print(a[:, 1])
print(a[::2, ::2])

J7. [OUT] One of these raises an error. Which, and what is the error message? [3]

import numpy as np
a = np.array([1, 2, 3, 4, 5, 6])
print(a.reshape(2, 3))
print(a.reshape(3, 2))
print(a.reshape(6, 1))
print(a.reshape(4, 2))

State the rule that decides whether a reshape is legal.