Indexing and Slicing
In one line: basic indexing with integers and slices returns a view; anything fancier returns a copy.
That distinction is the whole note. Everything else is syntax.
Basic indexing
a = np.arange(24).reshape(2, 3, 4)
a[0] # first slab → shape (3, 4)
a[0, 1] # → shape (4,)
a[0, 1, 2] # a scalar
a[0][1][2] # same value, but three temporary arrays — don't
a[:, 1, :] # → shape (2, 4)
a[..., 0] # Ellipsis: "all remaining axes" → shape (2, 3)
a[::2] # every other
a[::-1] # reversed
a[-1] # last
Use the comma form a[0, 1, 2], not the chained form a[0][1][2]. The chained form creates intermediate views at every step and is measurably slower.
... (Ellipsis) is the idiom for "index the last axis regardless of how many there are" — a[..., 0] works for 2-D, 3-D or 5-D input.
Slices are half-open and 0-based
seq[0:3] # elements 0, 1, 2 — three of them
stop is excluded. This matches BED coordinates and not GFF/VCF/HGVS coordinates — see Coordinate Systems, which is where the off-by-one bugs in your genomics code will come from.
Slices are views
a = np.arange(10)
b = a[2:5]
b[0] = 999
a # array([0, 1, 999, 3, 4, ...]) ← a changed
b.base is a # True
No copy was made. b is a window onto a's memory. This is a feature — slicing a 10 GB array is instant — but it means functions that slice and modify silently mutate their caller's data.
b = a[2:5].copy() # explicit, safe
Full discussion in Views vs Copies.
Assignment through a slice
a[2:5] = 0 # broadcast a scalar into the window
a[2:5] = [1, 2, 3] # shape must match
a[a > 100] = 100 # clip via boolean mask
a[:, 0] = new_column
This is in-place mutation, which is the fast path — no new allocation.
The three indexing modes
| Mode | Syntax | Returns |
|---|---|---|
| Basic | integers, slices, ..., None |
view |
| Boolean | a[mask] |
copy — see Boolean Masking |
| Integer array ("fancy") | a0, 3, 5 |
copy — see Fancy Indexing |
Mixing modes gets subtle. a[0, [1, 2]] mixes basic and fancy; a[[0, 1], [1, 2]] pairs the indices elementwise rather than taking a submatrix. When you want a submatrix, use np.ix_.
Adding and dropping axes
v = np.arange(3) # shape (3,)
v[None, :] # (1, 3)
v[:, None] # (3, 1)
v[..., None] # (3, 1)
a[:, 0] # drops the axis → shape (n,)
a[:, 0:1] # keeps it → shape (n, 1)
np.squeeze(a) # drop all length-1 axes
An integer index removes an axis; a length-1 slice keeps it. This trips people up constantly when a downstream function demands 2-D input.
Bioinformatics examples
# expression matrix: genes × samples
expr[0] # first gene, all samples
expr[:, 3] # fourth sample, all genes → shape (n_genes,)
expr[:, 3:4] # same data, shape (n_genes, 1) — keeps the axis
# extract a genomic window (converting from 1-based closed GFF coords)
region = coverage[start1 - 1 : end1]
# reverse complement of a one-hot encoded sequence: reverse position, flip base order
rc = onehot[::-1, ::-1]
# every third base — codon position 1
first_positions = seq_array[0::3]
Common mistakes
- Modifying a slice and being surprised the original changed. Or the reverse: expecting a slice to be a view when the operation actually produced a copy.
a[0][1] = xon a copy-producing first step — the write goes to a temporary and vanishes. This is the NumPy analogue of pandas chained assignment. See Copy-on-Write.- Assuming
a[[0,1], [2,3]]is a submatrix. It is[a[0,2], a[1,3]]. Usea[np.ix_([0,1],[2,3])]. - Off-by-one at format boundaries. Python slicing is half-open; VCF/GFF/HGVS are closed. See Coordinate Systems.
- Negative index confusion with slices.
a[-1]is the last element;a[:-1]is everything except the last;a[::-1]is reversed. - Out-of-bounds slicing is silent.
np.arange(5)[2:100]returns 3 elements, no error. Out-of-bounds integer indexing does raise. This asymmetry hides bugs at sequence ends.
See also
ndarray · Views vs Copies · Boolean Masking · Fancy Indexing · Coordinate Systems · loc vs iloc
Test cases · 2
| # | via | input | expected stdout |
|---|---|---|---|
| 1 | stdin | 10 12 15 9 4 30 22 2 5 | [15, 9, 4] |
| 2 | stdin | 1 2 3 4 5 0 3 | [1, 2, 3] |
Hints · 2
01Hint
Slices are start:stop, with stop excluded.
02Hint
A negative index counts back from the end: arr[-1] is the last element.