NumPy is the foundation layer of scientific Python. pandas DataFrames, TensorFlow tensors, scikit-learn models, and matplotlib plots all store their data in NumPy arrays under the hood. Skipping NumPy and jumping straight to pandas or ML frameworks is the most common reason people hit walls they cannot debug. In 2026, NumPy 2.x has tightened copy semantics and improved interoperability with Arrow and DLPack. Here is the learning path.
What changed in 2026
- NumPy 2.0 enforces stricter copy semantics.
array.copy() is explicit; many operations that returned views in 1.x now return copies and vice versa. This breaks some old pandas and scikit-learn integrations — check your library versions.
- DLPack support is first-class. Zero-copy sharing between NumPy, PyTorch, TensorFlow, and JAX arrays via the DLPack protocol — no more manual
.numpy() calls in most frameworks.
- StringDType is stable. Variable-length strings in NumPy arrays are now first-class, stored efficiently without boxing to Python objects.
- Python 3.12+ JIT compilation gains partial NumPy acceleration in CPython's new trace-based JIT (experimental).
The learning order that works
- ndarray basics — create, shape, dtype, reshape.
- Indexing and slicing — 1D, 2D, boolean masks, fancy indexing.
- Math operations — element-wise, reduction, axis parameter.
- Broadcasting — the four rules, then practice on 3-4 examples until they click.
- Views vs copies — when does an operation return a view? When does it copy?
- Linear algebra —
np.dot, np.linalg.solve, np.linalg.eig.
- Random —
np.random.default_rng() (the new generator API, not np.random.seed).
- Performance —
np.vectorize (convenience), numba.njit (real speed), memory layout (C vs F order).
ndarray fundamentals
import numpy as np
# Creation
a = np.array([1, 2, 3, 4, 5], dtype=np.float32)
b = np.zeros((3, 4)) # shape (3, 4), float64
c = np.arange(0, 10, 0.5) # like Python range but float-safe
d = np.linspace(0, 1, 100) # 100 evenly spaced points
# Inspect
print(a.shape, a.dtype, a.ndim, a.nbytes)
# (5,) float32 1 20
Indexing and boolean masks
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(m[1, 2]) # 6
print(m[:, 1]) # [2 5 8] — entire column 1
print(m[m > 5]) # [6 7 8 9] — boolean mask returns flat array
# Set all values above 5 to zero
m[m > 5] = 0
Broadcasting rules (the four rules)
# Rule: dimensions are aligned right-to-left.
# Sizes must be equal or one of them is 1 (expanded automatically).
a = np.ones((3, 1)) # shape (3, 1)
b = np.ones((1, 4)) # shape (1, 4)
c = a + b # shape (3, 4) — no copy of data, NumPy expands views
# Classic: add a row vector to every row of a matrix
matrix = np.zeros((5, 3)) # shape (5, 3)
bias = np.array([1, 2, 3]) # shape (3,) → treated as (1, 3)
result = matrix + bias # shape (5, 3) — each row gets bias added
Views vs copies — the source of most bugs
a = np.arange(6)
# Slices are VIEWS — modifying them modifies the original
b = a[2:5]
b[0] = 99
print(a) # [0 1 99 3 4 5] — a was modified!
# Explicit copy to avoid this
c = a[2:5].copy()
c[0] = 0
print(a) # unchanged
NumPy performance vs Python
| Operation |
Python loop |
NumPy vectorized |
Speedup |
| Sum 10M floats |
~1000 ms |
~10 ms |
~100x |
| Element-wise multiply |
~800 ms |
~8 ms |
~100x |
| Matrix multiply (1000x1000) |
impractical |
~5 ms (BLAS) |
10000x+ |
| Boolean filter on 1M items |
~200 ms |
~3 ms |
~65x |
How to pick the right operation
- Reduction? Use
np.sum, np.mean, np.max with the axis argument.
- Element-wise math? Use
+, *, np.exp, np.log directly on arrays.
- Condition on elements? Use boolean masks or
np.where.
- Linear algebra? Use
np.linalg or @ operator for matrix multiply.
- Need a random array? Use
rng = np.random.default_rng(42); rng.standard_normal(shape).
Common mistakes
Forgetting the axis parameter. np.mean(matrix) averages everything. np.mean(matrix, axis=0) averages column-wise. Most bugs come from missing or wrong axis.
Mutating views unintentionally. If you get an array from another array by slicing, you have a view. Modifications propagate back. Call .copy() when you want isolation.
Using np.vectorize for performance. It is a convenience wrapper around a Python loop — it looks vectorized but is not. For real JIT acceleration use Numba.
Mixing dtypes carelessly. Summing float32 and float64 upcasts silently. In tight memory budgets, this doubles your memory use.
Old random API. np.random.seed() + np.random.randn() is the legacy API. Use np.random.default_rng(seed) for reproducible, thread-safe random generation.
What to skip
np.matrix — deprecated; use 2D ndarray with the @ operator instead.
np.vectorize for hot paths — use Numba's @njit or move the operation to a framework tensor if you care about speed.
- Manual memory management in Python — let NumPy handle contiguous allocation; do not pre-allocate with lists and convert.
FAQ
Do I need NumPy if I am only using pandas?
Yes — pandas operations that go wrong are debugged at the NumPy level. Understanding ndarray shapes and dtypes is required to fix most pandas errors.
NumPy vs PyTorch tensors — what is the difference?
NumPy arrays live on CPU. PyTorch tensors can live on GPU and support autograd. PyTorch uses DLPack to zero-copy share with NumPy. For pure numerical computing on CPU, NumPy is simpler.
How long does NumPy take to learn?
Core indexing and broadcasting take a focused weekend. Real fluency (knowing when an operation returns a view, writing efficient vectorized code) takes 2–3 weeks of practice on real data.
Does NumPy support GPU?
Not directly. CuPy is a drop-in GPU replacement; JAX wraps NumPy-style APIs and runs on GPU/TPU. For deep learning, use framework tensors instead.
Where to go next