The NumPy operations you look up every time — from array creation to broadcasting, linear algebra, random sampling, and performance tips. This reference covers the full scientific Python workflow.
Quick reference
The 25 patterns that cover 90% of daily NumPy work.
| Pattern | Code |
|---|---|
| Import | import numpy as np |
| Create array | np.array([1, 2, 3]) |
| Zeros | np.zeros((3, 4)) |
| Ones | np.ones((2, 3)) |
| Range | np.arange(0, 10, 2) |
| Linspace | np.linspace(0, 1, 50) |
| Shape | a.shape → (3, 4) |
| Reshape | a.reshape(4, 3) |
| Flatten | a.flatten() |
| Data type | a.dtype |
| Cast type | a.astype(np.float32) |
| Index | a[1, 2] or a[1][2] |
| Slice row | a[1, :] |
| Slice col | a[:, 2] |
| Boolean mask | a[a > 5] |
| Sum | a.sum() / a.sum(axis=0) |
| Mean | a.mean() / a.mean(axis=1) |
| Dot product | a @ b or np.dot(a, b) |
| Stack | np.vstack([a, b]) / np.hstack([a, b]) |
| Concatenate | np.concatenate([a, b], axis=0) |
| Unique | np.unique(a) |
| Sort | np.sort(a) / np.argsort(a) |
| Where | np.where(a > 0, a, 0) |
| Random int | np.random.randint(0, 10, size=(3, 3)) |
| Random float | rng.random((3, 3)) |
Installation and setup
pip install numpy
import numpy as np
# Always pin versions in production
# pip install numpy==2.0.0
# Check version
print(np.__version__)
Creating arrays
From Python data
# 1D array
a = np.array([1, 2, 3, 4, 5])
print(a.shape) # (5,)
print(a.dtype) # int64 (platform dependent)
# 2D array
m = np.array([[1, 2, 3],
[4, 5, 6]])
print(m.shape) # (2, 3)
# Specify dtype
f = np.array([1, 2, 3], dtype=np.float64)
c = np.array([1+2j, 3+4j], dtype=np.complex128)
Built-in constructors
np.zeros((3, 4)) # 3×4 of 0.0
np.ones((2, 3)) # 2×3 of 1.0
np.full((2, 2), 7) # 2×2 of 7
np.eye(4) # 4×4 identity matrix
np.empty((2, 3)) # uninitialised (fast allocation)
# Ranges
np.arange(0, 10, 2) # [0 2 4 6 8] (like range())
np.arange(0.0, 1.0, 0.1) # works with floats too
# Evenly spaced points
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ]
np.logspace(0, 3, 4) # [1. 10. 100. 1000.]
# Like another array
np.zeros_like(m) # same shape/dtype as m, filled 0
np.ones_like(m)
np.full_like(m, -1)
From files
# CSV → array (no header)
a = np.loadtxt("data.csv", delimiter=",")
# CSV with header → skip first row
a = np.loadtxt("data.csv", delimiter=",", skiprows=1)
# Binary format (faster)
np.save("array.npy", a)
a = np.load("array.npy")
# Multiple arrays
np.savez("arrays.npz", x=a, y=b)
data = np.load("arrays.npz")
a = data["x"]
Array properties
a = np.array([[1, 2, 3], [4, 5, 6]])
a.shape # (2, 3) — tuple of dimension sizes
a.ndim # 2 — number of dimensions
a.size # 6 — total number of elements
a.dtype # dtype('int64')
a.itemsize # 8 — bytes per element
a.nbytes # 48 — total bytes (size × itemsize)
Indexing and slicing
Basic indexing
a = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
a[0, 0] # 10 — row 0, col 0
a[-1, -1] # 90 — last row, last col
a[1, :] # [40 50 60] — entire row 1
a[:, 2] # [30 60 90] — entire col 2
a[0:2, 1:] # [[20 30] [50 60]] — submatrix
Fancy indexing
# Integer array indexing
rows = np.array([0, 2])
cols = np.array([1, 2])
a[rows, cols] # [20, 90] — pairs (0,1) and (2,2)
# Select specific rows
a[[0, 2]] # rows 0 and 2
# Boolean indexing (masking)
mask = a > 40
a[mask] # [50 60 70 80 90]
a[a > 40] # same, inline
# Assign via mask
a[a < 30] = 0 # zero out elements below 30
np.where
# where(condition, value_if_true, value_if_false)
np.where(a > 50, a, 0) # keep values > 50, else 0
np.where(a > 50, 1, -1) # +1 / -1 flag array
indices = np.where(a == 50) # returns (row_indices, col_indices)
Reshaping and stacking
Reshape
a = np.arange(12) # [0 1 2 ... 11]
a.reshape(3, 4) # 3 rows, 4 cols
a.reshape(4, 3) # 4 rows, 3 cols
a.reshape(2, 2, 3) # 3D array
a.reshape(-1, 3) # -1 = infer automatically → (4, 3)
# Flatten (returns copy) vs ravel (returns view when possible)
a.flatten() # always copy
a.ravel() # view if possible (faster)
# Add/remove dimensions
a.reshape(1, 12) # (1, 12)
a[np.newaxis, :] # same — add axis at position 0
a.squeeze() # remove dimensions of size 1
Stack and split
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 1D arrays → 2D
np.vstack([a, b]) # [[1 2 3] [4 5 6]] vertical stack (row-wise)
np.hstack([a, b]) # [1 2 3 4 5 6] horizontal stack (col-wise for 1D)
# Stack along new axis
np.stack([a, b], axis=0) # [[1 2 3] [4 5 6]] same as vstack
np.stack([a, b], axis=1) # [[1 4] [2 5] [3 6]]
# General concatenate
np.concatenate([a, b], axis=0)
# Split
np.split(a, 3) # [array([1]), array([2]), array([3])]
np.array_split(a, 4) # uneven splits allowed
np.vsplit(m, 2) # split matrix along rows
np.hsplit(m, 3) # split matrix along cols
Math and statistics
Element-wise operations
a = np.array([1.0, 4.0, 9.0])
b = np.array([2.0, 2.0, 3.0])
a + b # [3. 6. 12.]
a - b # [-1. 2. 6.]
a * b # [2. 8. 27.]
a / b # [0.5 2. 3.]
a ** 2 # [1. 16. 81.]
a % b # [1. 0. 0.]
# Universal functions (ufuncs)
np.sqrt(a) # [1. 2. 3.]
np.abs(a)
np.exp(a)
np.log(a) # natural log
np.log2(a)
np.log10(a)
np.sin(a)
np.cos(a)
np.ceil(a)
np.floor(a)
np.round(a, 2)
np.clip(a, 0, 5) # clamp values to [0, 5]
Reductions
a = np.array([[1, 2, 3], [4, 5, 6]])
a.sum() # 21 — total
a.sum(axis=0) # [5 7 9] — column sums
a.sum(axis=1) # [6 15] — row sums
a.min() # 1
a.max() # 6
a.argmin() # 0 — flat index of minimum
a.argmax() # 5 — flat index of maximum
a.argmin(axis=0) # column-wise argmin
a.mean() # 3.5
a.std() # standard deviation
a.var() # variance
np.median(a) # 3.5
np.percentile(a, 75) # 75th percentile
np.cumsum(a, axis=1) # cumulative sum along cols
np.cumprod(a)
np.diff(a, axis=1) # first-order differences
Broadcasting
Broadcasting lets NumPy operate on arrays with different shapes — no loops, no copies.
Rules (applied from trailing dimensions)
Shape A: (3, 1, 4)
Shape B: (5, 4)
Result: (3, 5, 4)
Rule: dimensions must be equal OR one of them must be 1.
Examples
a = np.array([[1], [2], [3]]) # shape (3, 1)
b = np.array([10, 20, 30]) # shape (3,) → treated as (1, 3)
a + b
# [[11 21 31]
# [12 22 32]
# [13 23 33]]
# Normalise each row (subtract row mean)
m = np.array([[1, 2, 3], [4, 5, 6]])
m - m.mean(axis=1, keepdims=True)
# [[-1. 0. 1.]
# [-1. 0. 1.]]
# keepdims=True preserves shape (2,1) so broadcasting works
Linear algebra
import numpy as np
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
# Matrix multiplication
a @ b # preferred syntax (Python 3.5+)
np.matmul(a, b) # same
np.dot(a, b) # also works for 2D
# Element-wise vs matrix multiply
a * b # element-wise (Hadamard product)
a @ b # matrix multiply
# Transpose
a.T
a.transpose()
# Linear algebra functions
np.linalg.det(a) # determinant
np.linalg.inv(a) # inverse
np.linalg.norm(a) # Frobenius norm by default
np.linalg.norm(a, ord=2) # spectral norm
np.linalg.eig(a) # eigenvalues and eigenvectors
np.linalg.svd(a) # singular value decomposition
np.linalg.solve(a, b) # solve Ax = b (prefer over inv(a) @ b)
# Matrix powers
np.linalg.matrix_power(a, 3)
Random numbers
# Modern API — always prefer rng over legacy np.random.*
rng = np.random.default_rng(seed=42) # reproducible
rng.random((3, 3)) # uniform [0, 1)
rng.integers(0, 10, size=5) # integers in [0, 10)
rng.normal(loc=0, scale=1, size=(2, 3)) # normal distribution
rng.uniform(low=-1, high=1, size=100)
rng.choice(np.arange(10), size=5, replace=False) # without replacement
rng.shuffle(a) # in-place shuffle
rng.permutation(a) # shuffled copy
# Distributions
rng.binomial(n=10, p=0.5, size=1000)
rng.poisson(lam=3, size=1000)
rng.exponential(scale=1.0, size=1000)
# Legacy API (avoid in new code)
# np.random.seed(42) # global seed — not thread-safe
# np.random.rand(3, 3) # same as rng.random
Data types
| dtype | Description | Bytes |
|---|---|---|
np.bool_ |
Boolean | 1 |
np.int8 |
Signed 8-bit | 1 |
np.int16 |
Signed 16-bit | 2 |
np.int32 |
Signed 32-bit | 4 |
np.int64 |
Signed 64-bit | 8 |
np.uint8 |
Unsigned 8-bit | 1 |
np.float16 |
Half precision | 2 |
np.float32 |
Single precision | 4 |
np.float64 |
Double precision | 8 |
np.complex64 |
2× float32 | 8 |
np.complex128 |
2× float64 | 16 |
a = np.array([1, 2, 3])
a.astype(np.float32) # cast
a.astype("f4") # shorthand: f4 = float32, i4 = int32, u1 = uint8
Sorting and searching
a = np.array([3, 1, 4, 1, 5, 9, 2, 6])
np.sort(a) # sorted copy
np.sort(a)[::-1] # descending
a.sort() # in-place (no return value)
np.argsort(a) # indices that would sort a
a[np.argsort(a)] # same as np.sort(a)
# Structured: sort 2D by column
m = np.array([[3, 1], [1, 4], [2, 2]])
m[m[:, 0].argsort()] # sort rows by first column
# Searching
np.searchsorted(np.sort(a), 4) # insertion point in sorted array
np.nonzero(a) # indices of non-zero elements
np.argmin(a) # index of min
np.argmax(a) # index of max
# Set operations on arrays
np.unique(a)
np.intersect1d(a, b)
np.union1d(a, b)
np.setdiff1d(a, b) # in a but not b
np.isin(a, [1, 3]) # boolean mask
Practical patterns
Normalise to [0, 1]
def normalise(a):
lo, hi = a.min(), a.max()
return (a - lo) / (hi - lo)
Standardise (z-score)
def standardise(a):
return (a - a.mean()) / a.std()
Moving average
def moving_avg(a, window):
return np.convolve(a, np.ones(window) / window, mode="valid")
One-hot encode
def one_hot(labels, num_classes):
result = np.zeros((len(labels), num_classes))
result[np.arange(len(labels)), labels] = 1
return result
labels = np.array([0, 2, 1])
one_hot(labels, 3)
# [[1. 0. 0.]
# [0. 0. 1.]
# [0. 1. 0.]]
Batch processing
def batch(data, size):
for i in range(0, len(data), size):
yield data[i : i + size]
for chunk in batch(large_array, 1000):
process(chunk) # avoids loading all at once
Euclidean distance matrix
# Distance between every pair of rows in X — no Python loops
def pairwise_distances(X):
diff = X[:, np.newaxis, :] - X[np.newaxis, :, :] # (n, n, d)
return np.sqrt((diff ** 2).sum(axis=-1)) # (n, n)
Performance tips
# 1. Avoid Python loops — use vectorised operations
# SLOW
result = [x**2 for x in a]
# FAST
result = a ** 2
# 2. Use views, not copies when possible
b = a[1:4] # view — no memory allocated
b = a[1:4].copy() # copy — needed if you mutate b
# 3. Use float32 over float64 for ML/GPU work (2× memory)
weights = np.zeros((1000, 1000), dtype=np.float32)
# 4. Prefer in-place operations
a += 1 # in-place (no new allocation)
a = a + 1 # creates new array
# 5. Contiguous memory layout matters
a = np.ascontiguousarray(a) # C-contiguous (row-major)
a = np.asfortranarray(a) # Fortran-contiguous (col-major)
# 6. Check with np.may_share_memory
np.may_share_memory(a, b) # True if they share memory
# 7. Einstein summation (compact, often faster)
np.einsum("ij,jk->ik", a, b) # matrix multiply
np.einsum("ii->i", a) # diagonal elements
np.einsum("ij->i", a) # row sums
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
a = np.array([1,2,3]); b = a; b[0] = 99 |
a is also modified — arrays are references |
b = a.copy() |
np.random.seed(42) in threaded code |
Global state causes race conditions | Use rng = np.random.default_rng(42) per thread |
np.dot(a, b) where a is 3D |
Behaviour differs from @ for N-D arrays |
Use @ (matmul) for ≥2D |
a.sort() expecting a return value |
In-place sort returns None |
Use np.sort(a) for a copy |
a[mask] = value changing a view |
Might not propagate to original | Ensure you are working on the original, not a slice |
float division in older code |
np.int64 / np.int64 returns float in NumPy ≥1.20 but be explicit |
Cast: a.astype(float) / b |
a.reshape(3, 4) when size doesn't match |
ValueError |
Check a.size == 12 first; use -1 for one dim |
FAQ
NumPy vs pandas — when to use which?
NumPy operates on typed N-D arrays of a single dtype (fast math). pandas builds on NumPy and adds labelled axes, mixed dtypes, and a rich data-manipulation API. Use NumPy for numerical algorithms and low-level operations; use pandas for tabular data with named columns.
Why does a[1:3] not copy?
NumPy slices return views — objects that point into the same memory buffer. This avoids allocation but means mutations affect the original. Call .copy() when you need independence.
How do I avoid the "ambiguous truth value" error?if a raises ValueError when a has more than one element. Use a.any(), a.all(), or index a scalar: if a[0].
What is keepdims=True?
Reductions collapse an axis. keepdims=True preserves that axis as size-1, so broadcasting still works: a - a.mean(axis=1, keepdims=True) subtracts each row's mean from that row.
How do I speed up a slow NumPy loop?
Try in order: (1) vectorise with ufuncs, (2) np.einsum, (3) Numba @jit, (4) Cython, (5) CuPy for GPU. Profile with %timeit or cProfile before optimising.
np.nan comparisons always return False — why?
IEEE 754: NaN != NaN. Use np.isnan(a) to detect missing values, and np.nanmean / np.nansum to compute statistics that ignore them.