Introduction
NumPy is the foundation underneath almost every Python tool that touches numbers — pandas, scikit-learn, TensorFlow, PyTorch, OpenCV. If you’re aiming at data science, machine learning, or any scientific computing in Python, NumPy isn’t optional. It’s the layer your frameworks talk to.
The two things that make NumPy worth learning: the n-dimensional array (called ndarray)
that’s 30–100× faster than equivalent Python lists for numerical work, and a giant
library of math functions that operate on entire arrays without writing loops. This walkthrough covers
every fundamental you’ll actually use.
📚 Table of contents
- Why NumPy is faster than Python lists
- Installation and import convention
- Creating arrays (six common ways)
- Data types and memory
- Multi-dimensional arrays: shape, ndim, size
- Indexing and slicing in 1D, 2D, 3D
- Boolean indexing
- Array operations: element-wise, scalar, vectorized
- Reshaping, flattening, transposing, concatenating
- Understanding axes
- Statistical operations
- Linear algebra
- Useful methods you’ll reach for daily
- Practical example: image processing
- Practical example: data analysis
- Common mistakes
- FAQs
Why NumPy is faster than Python lists
A Python list stores pointers to arbitrary Python objects. A NumPy ndarray stores a
contiguous block of same-typed values laid out tightly in memory, with all operations
implemented in C. The difference matters: on a 1,000,000-element array, adding 1 to every element runs
roughly 30× faster in NumPy than in a Python list
comprehension — ~0.001 s versus ~0.035 s in typical benchmarks.
The reason isn’t magic. NumPy can’t branch on element type during the loop because every
element has the same type. It also vectorizes operations — you write
arr + 10, not [x + 10 for x in arr]. The vectorized version skips the Python
interpreter entirely.
Installation and import convention
pip install numpy
# or: pip3 install numpy
Convention everyone follows: import numpy as np. Every tutorial, every codebase, every
Stack Overflow answer uses np as the alias. Do the same. A Jupyter notebook is the easiest
place to follow along — the cell-by-cell flow matches how you actually explore data with NumPy.
Creating arrays (six common ways)
import numpy as np
# 1. From a Python list
a = np.array([1, 2, 3, 4, 5])
# 2. A range of values
b = np.arange(0, 10, 2) # [0 2 4 6 8]
# 3. N evenly spaced numbers between two endpoints
c = np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1.]
# 4. Zeros, ones, and a constant
zeros = np.zeros((3, 4)) # 3x4 of 0.0
ones = np.ones(5)
sevens = np.full((2, 2), 7)
# 5. An identity matrix
eye = np.eye(3)
# 6. Random values
rand = np.random.rand(5) # uniform 0..1
ints = np.random.randint(0, 10, 5) # integers
Important rule: all elements must be the same type. You can’t mix ints with strings the way you would in a Python list. Mix ints and floats and NumPy promotes everything to float64 to fit both.
Data types and memory
Every array has a dtype — int32, int64,
float32, float64, bool, and so on. You can read it, set it, and
convert it:
a = np.array([1, 2, 3])
a.dtype # int64 (or int32 on Windows)
b = np.array([1, 2, 3], dtype=np.float32)
c = a.astype(np.float64) # returns a new array
Why care? Memory and precision. float32 is 4 bytes per element; float64 is
8. Multiply by a million elements and you’re looking at 4 MB vs 8 MB — meaningful when you
start handling images, audio, or tensors at scale.
Multi-dimensional arrays: shape, ndim, size
The shape is a tuple describing how many elements live in each dimension. The number of entries in the tuple equals the number of dimensions.
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
m.shape # (3, 3)
m.ndim # 2
m.size # 9
m.dtype # int64
# 3D — think "pages of rows and columns"
cube = np.zeros((2, 3, 4)) # 2 pages, 3 rows, 4 cols
cube.shape # (2, 3, 4)
Shape works outermost-to-innermost. (2, 3, 4) means “2 of those, each containing 3
of those, each containing 4 elements.” This mental model carries you through 4D, 5D, and
beyond.
Indexing and slicing in 1D, 2D, 3D
1D arrays index just like Python lists. The interesting part starts with 2D: you put both indices inside one set of brackets, separated by commas.
a = np.array([10, 20, 30, 40, 50])
a[0] # 10
a[-1] # 50
a[1:4] # [20 30 40]
a[::2] # [10 30 50] — every other element
m = np.arange(1, 10).reshape(3, 3) # 3x3
m[1, 2] # row 1, col 2 -> 6
m[0, :] # entire first row
m[:, 1] # entire second column
m[0:2, 0:2] # 2x2 sub-matrix from top-left
The comma separates dimensions. A colon means “all of this dimension.” Same logic scales
to 3D and beyond — cube[0, :, 1] reads “page 0, all rows, column 1.”
Boolean indexing
Powerful enough to deserve its own section. Pass a boolean array as the index and NumPy returns only the elements where the boolean is True:
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
arr[arr > 5] # [6 7 8 9 10]
arr[arr % 2 == 0] # [2 4 6 8 10]
This pattern shows up everywhere in data work — filter a dataset, select rows, mask invalid
values. Combine boolean masks with & and | (not and /
or) for compound conditions.
Array operations: element-wise, scalar, vectorized
Every arithmetic operator on a NumPy array is element-wise:
a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])
a + b # [6 8 10 12] element-wise
a * b # [5 12 21 32] element-wise multiplication (not dot product)
a + 10 # [11 12 13 14] scalar broadcast
np.sqrt(a) # [1. 1.414 1.732 2.]
a > 2 # [False False True True] boolean array
Note: a * b is element-wise multiplication, not the linear-algebra dot product.
Use np.dot(a, b) or a @ b for matrix multiplication.
Reshaping, flattening, transposing, concatenating
a = np.arange(12)
a.reshape(3, 4) # 3x4 matrix
a.reshape(2, -1) # 2 rows, NumPy figures out the cols
a.reshape(3, 4).flatten() # back to 1D
m = np.array([[1, 2], [3, 4]])
m.T # transpose -> [[1 3] [2 4]]
# Concatenation
np.vstack([m, m]) # stack vertically (4x2)
np.hstack([m, m]) # stack horizontally (2x4)
np.concatenate([m, m], axis=0) # same as vstack
The -1 placeholder in reshape is the everyday escape hatch — you fix
one dimension, let NumPy compute the other.
Understanding axes (the part that trips everyone up)
When you call arr.sum(axis=0), what are you summing? This is the most-misunderstood part
of NumPy. The rule:
An axis is the dimension you collapse. axis=0
collapses the first (outermost) dimension; axis=1 collapses the second; and so on.
The result has one fewer dimension than the input.
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
m.sum() # 45 (everything)
m.sum(axis=0) # [12 15 18] — sums each column
m.sum(axis=1) # [6 15 24] — sums each row
Memory hook: “axis 0 is rows” means you collapse rows, which gives one number per column. Once that clicks, every reduction (mean, std, min, max, argmax) follows the same logic.
Statistical operations
arr.mean(),arr.median()— central tendencyarr.std(),arr.var()— spreadarr.min(),arr.max()— extremesarr.argmin(),arr.argmax()— index of the min/maxarr.sum(),arr.cumsum()— totals and running totals
All of these accept an axis argument, so the same call works on 1D, 2D, or 3D arrays.
argmax is one to remember — it’s how you find “the index of the best
student” or “the row with the highest score.”
Linear algebra
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
np.dot(a, b) # 32 (dot product)
a @ b # same, infix operator
M = np.array([[1, 2], [3, 4]])
np.linalg.det(M) # determinant
np.linalg.inv(M) # inverse
np.linalg.eig(M) # eigenvalues + eigenvectors
NumPy ships with the full np.linalg module — matrix inverse, determinant, SVD,
eigendecomposition, solve. If you’ve taken a linear algebra course, this is where it pays off.
Useful methods you’ll reach for daily
np.sort(arr)— sorted copy.arr.sort()sorts in place.np.unique(arr)— deduplicate.np.where(condition)— the indices where a condition is True. Two-argument form acts like a ternary:np.where(arr>0, arr, 0).np.clip(arr, lo, hi)— clamp values into a range. Perfect for image brightness.- Fancy indexing:
arr[[1, 3, 5]]picks specific positions.
Practical example: image processing
Images are NumPy arrays. A grayscale image is a 2D array of values 0–255; a color image is 3D
with shape (height, width, 3). Brightening and contrast adjustments are arithmetic on
those arrays.
image = np.random.randint(0, 256, size=(10, 10)) # tiny fake image
# Brighter, clipped to valid range
brighter = np.clip(image + 50, 0, 255)
# More contrast (multiply, then clip)
high_contrast = np.clip(image * 1.5, 0, 255)
Real image libraries (PIL/Pillow, OpenCV) hand you NumPy arrays, so everything you learn here transfers directly to actual pixel manipulation.
Practical example: data analysis
# rows = students, columns = tests
scores = np.array([
[85, 90, 88],
[92, 87, 91],
[78, 82, 80],
[95, 98, 96],
[88, 85, 90],
])
scores.mean(axis=1) # average per student
scores.mean(axis=0) # average per test
scores.argmax(axis=0) # index of best score on each test
best_student = scores.mean(axis=1).argmax() # row index
excellent = np.all(scores > 90, axis=1) # rows where ALL tests > 90
scores[excellent] # those students' scores
Five lines do the work that would be ten loops in pure Python — and run faster than a single list comprehension because the math happens in C.
❌ Common mistakes
- Confusing
*(element-wise) with matrix multiplication. Use@ornp.dotfor the latter. - Mixing
axis=0andaxis=1— remember, the axis is the one you collapse. - Forgetting that all elements share one dtype. Putting a string into an int array up-casts everything to object dtype and loses speed.
- Using Python
and/orwith boolean arrays. Use&/|with parentheses around each clause. - Reshaping with an incompatible total size — NumPy raises rather than silently dropping data.
- Looping over arrays in Python when a vectorized op exists. Almost always slower and uglier.
💡 Pro tips
- If you write a Python loop over a NumPy array, stop and ask if a vectorized version exists. Usually it does.
- Use
arr.shapeandarr.dtypelike print statements while debugging — shape mismatches are the #1 source of NumPy bugs. - Prefer
arr.reshape(-1, k)over hand-computing dimensions. NumPy will tell you if the total size is wrong. - Use
np.random.default_rng(seed)instead of the legacynp.randommodule for reproducible randomness. - When working with images or audio, the dtype matters — uint8 for pixels (0–255), float32 for normalized values (0..1).
Conclusion
NumPy isn’t a library to memorize — it’s a way of thinking. Arrays instead of lists, vectorized ops instead of loops, shapes and axes instead of nested indices. Once you internalize that, every data-science library you touch afterward feels familiar.
Next step: load real data with np.loadtxt or pandas, then revisit each section above with
your own array. The patterns repeat.
Related reading: traditional RAG vs vectorless RAG — how AI actually works (tokens & context) — LangChain review
Explore More on DevShelf
-
Learn Matplotlib in 30 Minutes
The natural next step — visualize the NumPy arrays you've built using Matplotlib's Figure/Axes model, scatter plots, and colormaps.
-
Python Skills You Need Before Machine Learning
NumPy mastery is Stage 2 of this 9-stage roadmap — see how it fits into the full ML-ready Python skill stack.