All topics
Data · Learning hub

NumPy notes for developers

Master NumPy with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Data notes
NumPy

Arrays, Data Types & Creation

NumPy: Arrays, Data Types & Creation NumPy is the foundation of the Python scientific computing ecosystem. Its ndarray stores homogeneous data in a contiguous m

NumPy: Arrays, Data Types & Creation

NumPy is the foundation of the Python scientific computing ecosystem. Its ndarray stores homogeneous data in a contiguous memory block — operations are implemented in C and 10–100× faster than pure Python loops.

Installation & Import

pip install numpy
import numpy as np

np.__version__   # '2.0.0'

Creating Arrays

# From Python lists
a = np.array([1, 2, 3])                      # 1D, shape (3,)
b = np.array([[1, 2, 3], [4, 5, 6]])         # 2D, shape (2, 3)
c = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])  # 3D, shape (2, 2, 2)

# Built-in creators
np.zeros((3, 4))             # 3×4 array of 0.0
np.ones((2, 3))              # 2×3 array of 1.0
np.full((2, 3), 7)           # 2×3 array of 7
np.eye(4)                    # 4×4 identity matrix
np.empty((3, 3))             # uninitialized (garbage values, faster)

# Ranges
np.arange(0, 10, 2)          # [0, 2, 4, 6, 8]  (like range, but returns array)
np.linspace(0, 1, 5)         # [0.0, 0.25, 0.5, 0.75, 1.0]  (evenly spaced)
np.logspace(0, 2, 3)         # [1, 10, 100]  (log-spaced)

# Random arrays
rng = np.random.default_rng(seed=42)   # recommended: new-style Generator
rng.random((3, 3))           # uniform [0, 1)
rng.integers(0, 10, size=(3, 3))  # integers [0, 10)
rng.normal(0, 1, size=(100,))    # standard normal distribution
rng.choice([1, 2, 3, 4, 5], size=3, replace=False)  # random sample

# From other sources
np.fromfunction(lambda i, j: i * j, (4, 4))   # computed by function
np.frombuffer(buffer, dtype=np.uint8)          # from bytes
np.load('array.npy')                           # load from file
np.loadtxt('data.csv', delimiter=',', skiprows=1)

Data Types (dtype)

# Specify dtype at creation
np.array([1, 2, 3], dtype=np.float32)
np.zeros((3, 3), dtype=np.int8)
np.ones((2, 2), dtype=np.complex128)

# Common dtypes
np.int8, np.int16, np.int32, np.int64       # signed integers (8/16/32/64-bit)
np.uint8, np.uint16, np.uint32, np.uint64   # unsigned integers
np.float16, np.float32, np.float64          # floating point (float64 = double)
np.complex64, np.complex128                  # complex numbers
np.bool_                                     # boolean
np.str_, np.bytes_                           # string types

# Check and convert
a.dtype                # dtype('float64')
a.astype(np.int32)     # convert (returns new array)
a.view(np.uint8)       # reinterpret bytes (no copy)

# Structured arrays (like records)
dt = np.dtype([('name', 'U20'), ('age', np.int32), ('score', np.float64)])
records = np.array([('Alice', 30, 95.5), ('Bob', 25, 87.3)], dtype=dt)
records['name']        # array(['Alice', 'Bob'])
records['score']       # array([95.5, 87.3])

Array Properties

a = np.zeros((3, 4, 5))

a.shape        # (3, 4, 5)
a.ndim         # 3  — number of dimensions
a.size         # 60 — total number of elements
a.dtype        # dtype('float64')
a.itemsize     # 8  — bytes per element
a.nbytes       # 480 — total bytes (size * itemsize)

# Reshape (must keep same total elements)
a = np.arange(12)       # shape (12,)
b = a.reshape(3, 4)     # shape (3, 4)
c = a.reshape(2, -1)    # -1 inferred: shape (2, 6)
d = a.reshape(2, 2, 3)  # 3D: shape (2, 2, 3)
e = a.flatten()         # always returns copy, shape (12,)
f = a.ravel()           # returns view when possible, shape (12,)

# Transpose
b.T                     # shape (4, 3)
np.transpose(b)         # same
a.swapaxes(0, 2)        # swap axes
NumPy

Indexing, Slicing & Broadcasting

NumPy: Indexing, Slicing & Broadcasting Basic Indexing & Slicing a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9,10,11,12]]) # Element access a[0, 1] # 2 (row 0, c

NumPy: Indexing, Slicing & Broadcasting

Basic Indexing & Slicing

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

# Element access
a[0, 1]         # 2  (row 0, col 1)
a[-1, -1]       # 12 (last row, last col)

# Slicing: [start:stop:step]
a[0, :]         # [1 2 3 4]  — first row
a[:, 0]         # [1 5 9]    — first column
a[0:2, 1:3]     # [[2,3],[6,7]]  — submatrix
a[::2, ::2]     # [[1,3],[9,11]] — every other row and col
a[::-1]         # reversed rows

# Slices are VIEWS — modifying slice modifies original
row = a[0, :]   # view
row[0] = 99     # modifies a!
a.copy()[0]     # explicit copy to avoid this

# 3D indexing
b = np.arange(24).reshape(2, 3, 4)
b[0, :, :]      # first "sheet"
b[:, 1, :]      # middle rows of all sheets
b[1, 2, 3]      # scalar element

Advanced Indexing

a = np.array([10, 20, 30, 40, 50])

# Integer array indexing (fancy indexing) — always returns copy
idx = np.array([0, 2, 4])
a[idx]          # [10 30 50]
a[[0, 0, 3]]    # [10 10 40]  — duplicates allowed

# 2D fancy indexing
b = np.arange(16).reshape(4, 4)
rows = np.array([0, 1, 2])
cols = np.array([1, 2, 3])
b[rows, cols]   # [b[0,1], b[1,2], b[2,3]] = [1, 6, 11]

# Boolean indexing — most common in data analysis
a = np.array([1, -2, 3, -4, 5])
mask = a > 0
a[mask]         # [1, 3, 5]  — only positive elements
a[a > 0]        # same, inline
a[a < 0] = 0    # set negatives to zero IN PLACE

# np.where — conditional element selection
np.where(a > 0, a, 0)          # positive as-is, others 0
np.where(a > 0, 'pos', 'neg')  # string labels

# np.nonzero / np.argwhere
np.nonzero(a > 0)               # tuple of index arrays
np.argwhere(a > 0)              # 2D array of indices

Broadcasting

Broadcasting lets NumPy operate on arrays with different shapes without copying data. Arrays are compatible when dimensions are equal or one of them is 1.

# Broadcasting rules (right-align shapes, pad with 1s on left):
# Shape (3, 4) + shape (4,)  → (3, 4) + (1, 4) → broadcast to (3, 4) ✓
# Shape (3, 1) + shape (1, 4) → broadcast to (3, 4) ✓
# Shape (3, 4) + shape (3,)  → (3, 4) + (3, 1)??? — ERROR: 4 ≠ 3 and 3 ≠ 1

# Scalar broadcasts to any shape
a = np.ones((3, 4))
a * 5               # every element × 5

# 1D row vector added to each row of 2D matrix
matrix = np.arange(12).reshape(3, 4)
row    = np.array([1, 2, 3, 4])        # shape (4,) → treated as (1, 4)
matrix + row        # shape (3, 4) — row added to each of 3 rows

# 1D column vector added to each column
col = np.array([[1], [2], [3]])         # shape (3, 1)
matrix + col        # shape (3, 4) — each row i gets col[i] added to all elements

# Practical: center data (subtract column means)
data = np.random.rand(100, 5)
means = data.mean(axis=0)              # shape (5,)
centered = data - means                # broadcasts: (100, 5) - (5,) → (100, 5)

# Normalize each row to unit length
norms = np.linalg.norm(data, axis=1, keepdims=True)  # shape (100, 1)
normalized = data / norms              # (100, 5) / (100, 1) → (100, 5)

# Outer product via broadcasting
a = np.array([1, 2, 3])    # shape (3,)
b = np.array([10, 20, 30]) # shape (3,)
a[:, np.newaxis] * b       # shape (3, 1) × (3,) → (3, 3) outer product
NumPy

Math, Linear Algebra & Performance

NumPy: Math, Linear Algebra & Performance Universal Functions (ufuncs) a = np.array([1.0, 4.0, 9.0, 16.0]) # Element-wise math (vectorized — no Python loop need

NumPy: Math, Linear Algebra & Performance

Universal Functions (ufuncs)

a = np.array([1.0, 4.0, 9.0, 16.0])

# Element-wise math (vectorized — no Python loop needed)
np.sqrt(a)              # [1. 2. 3. 4.]
np.square(a)            # [1. 16. 81. 256.]
np.abs(np.array([-1, -2, 3]))  # [1 2 3]
np.exp(a)               # e^x
np.log(a)               # natural log
np.log2(a)
np.log10(a)
np.sin(a)
np.cos(a)
np.floor(a)
np.ceil(a)
np.round(a, decimals=2)
np.clip(a, 0, 5)        # clamp values to [0, 5]

# Aggregations
a.sum()                  # sum all elements
a.sum(axis=0)            # sum along rows (result: shape per column)
a.sum(axis=1)            # sum along columns (result: shape per row)
a.mean()
a.std()
a.var()
a.min()
a.max()
a.argmin()               # index of minimum
a.argmax()               # index of maximum
a.cumsum()               # cumulative sum
np.median(a)
np.percentile(a, [25, 50, 75])
np.unique(a)
np.sort(a)               # returns sorted copy
a.sort()                 # sorts in-place

Linear Algebra

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

# Matrix multiplication
A @ B                    # preferred (PEP 465)
np.matmul(A, B)          # same
np.dot(A, B)             # also works for 2D

# Element-wise (NOT matrix multiply)
A * B                    # [[ 5,12],[21,32]]

# Linear algebra functions
np.linalg.det(A)         # determinant
np.linalg.inv(A)         # inverse
np.linalg.trace(A)       # sum of diagonal
np.linalg.norm(A)        # Frobenius norm by default
np.linalg.norm(A, axis=1)  # row norms

# Eigenvalues and eigenvectors
eigenvalues, eigenvectors = np.linalg.eig(A)

# Singular Value Decomposition
U, S, Vt = np.linalg.svd(A)

# Solve linear system Ax = b
b = np.array([1, 2])
x = np.linalg.solve(A, b)     # faster than inv(A) @ b

# Least squares (overdetermined system)
A_tall = np.random.rand(100, 3)
b_tall = np.random.rand(100)
x, residuals, rank, sv = np.linalg.lstsq(A_tall, b_tall, rcond=None)

Performance & Best Practices

  • Vectorize: never write Python loops over array elements. Use ufuncs, array operations, and built-in aggregations.

  • Avoid copies: slices return views; fancy indexing and boolean indexing return copies. Profile with a.base to check.

  • Memory layout: C-contiguous (row-major) for row operations; F-contiguous for column operations. Use np.ascontiguousarray() if needed.

  • dtype matters: float32 is 2× faster than float64 on GPU and many CPU SIMD operations. Use smallest dtype that fits.

  • np.einsum: Einstein summation notation for complex tensor operations — often faster than matmul chains.

  • numba: JIT-compiles Python/NumPy to LLVM — near-C speed for loops that can't be vectorized.

  • Use out= parameter to write results into pre-allocated array: np.add(a, b, out=result) — avoids allocation.

# Benchmark: Python loop vs NumPy vectorization
import time
n = 1_000_000
a = np.random.rand(n)

# Python loop — slow
start = time.time()
result = [x ** 2 for x in a]
print(f"Loop: {time.time() - start:.3f}s")  # ~0.3s

# NumPy vectorized — fast
start = time.time()
result = a ** 2
print(f"NumPy: {time.time() - start:.4f}s") # ~0.002s — 150× faster

# np.einsum examples
A = np.random.rand(100, 50)
B = np.random.rand(50, 30)
np.einsum('ij,jk->ik', A, B)   # matrix multiply (same as A @ B)
np.einsum('ij->i', A)          # row sums (same as A.sum(axis=1))
np.einsum('ii->', A[:50, :50]) # trace

# Save/load arrays
np.save('array.npy', a)                   # single array
np.savez('arrays.npz', a=a, b=B)         # multiple arrays
loaded = np.load('arrays.npz')
loaded['a']                               # retrieve by key
np.savetxt('data.csv', A, delimiter=',', fmt='%.4f')

Keep your NumPy knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever