🎯 What You'll Learn
- Why NumPy exists and why Python lists are too slow for ML
- Create arrays using
np.array,np.zeros,np.ones,np.arange,np.linspace, andnp.random - Understand
shape,ndim,size, anddtype— the four properties you'll check constantly - Index, slice, reshape, and apply boolean masks to arrays
- Understand how real-world ML data (images, tabular data) is represented as arrays
Install NumPy once with pip install numpy. Every lesson in this curriculum starts by importing it as import numpy as np — the np alias is a universal convention you'll see everywhere.
1 The Problem NumPy Solves
Python is a fantastic general-purpose language, but it was never designed for heavy numerical computation. When you create a Python list like [1.0, 2.5, 3.7], Python stores not just the numbers but also type information and other metadata for each element separately — it's an array of pointers to objects. This flexibility is great for mixed-type lists, but it's expensive when you want to add a million numbers together.
NumPy arrays solve this by storing all elements as the same type in a single contiguous block of memory. This design allows NumPy to call highly optimized C and Fortran routines under the hood. The result is that operations on NumPy arrays can be 100–1000× faster than equivalent Python loops.
import numpy as np
import time
# Python list: add 10 million numbers with a loop
python_list = list(range(10_000_000))
start = time.time()
total = sum(python_list)
print(f"Python list: {time.time() - start:.3f}s")
# NumPy array: add 10 million numbers
np_array = np.arange(10_000_000)
start = time.time()
total = np.sum(np_array)
print(f"NumPy array: {time.time() - start:.3f}s")
That's a 20× speedup on a simple sum. For ML workloads that involve billions of multiplications, the difference is the gap between training a model in hours versus weeks.
scikit-learn, TensorFlow, PyTorch, Pandas, SciPy, and virtually every other ML library either uses NumPy arrays internally or is directly compatible with them. Mastering NumPy is mastering the shared language of all of ML.
2 Creating Arrays
There are several ways to create NumPy arrays. Knowing all of them is important — different situations call for different creation methods.
From a Python List
The most direct way: pass any Python list (or list of lists) to np.array().
import numpy as np
# 1D array from a list
a = np.array([10, 20, 30, 40, 50])
print(a) # [10 20 30 40 50]
print(type(a)) # <class 'numpy.ndarray'>
# 2D array from a list of lists (rows x columns)
b = np.array([[1, 2, 3],
[4, 5, 6]])
print(b)
# [[1 2 3]
# [4 5 6]]
Arrays Filled with Zeros, Ones, or a Constant
When you're building ML models, you'll frequently need to initialize arrays before filling them with computed values. These functions are your go-to for that.
zeros = np.zeros((3, 4)) # 3x4 matrix of 0.0
ones = np.ones((2, 5)) # 2x5 matrix of 1.0
full = np.full((3, 3), 7) # 3x3 matrix of 7
eye = np.eye(4) # 4x4 identity matrix (1s on diagonal)
print(zeros)
# [[0. 0. 0. 0.]
# [0. 0. 0. 0.]
# [0. 0. 0. 0.]]
print(np.eye(3))
# [[1. 0. 0.]
# [0. 1. 0.]
# [0. 0. 1.]]
Sequences with arange and linspace
These are the NumPy equivalents of Python's range(), but they return arrays and support floating-point steps.
# np.arange(start, stop, step) — like range() but returns an array
r = np.arange(0, 10, 2) # [0, 2, 4, 6, 8] — stop is exclusive
r2 = np.arange(1, 10, 3) # [1, 4, 7]
# np.linspace(start, stop, num) — evenly spaced INCLUDING both endpoints
l = np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ]
l2 = np.linspace(0, 100, 6) # [ 0. 20. 40. 60. 80. 100.]
print(r) # [0 2 4 6 8]
print(l) # [0. 0.25 0.5 0.75 1. ]
Use arange when you know the step size (e.g., every 2 units). Use linspace when you know the number of points (e.g., 100 evenly spaced values between 0 and 1). In ML, linspace is common for creating axes for plots and test inputs.
It can be hard to picture what "evenly spaced values" actually looks like from the printed array alone. Drag the slider to change how many points np.linspace(0, 10, num) generates and watch the points redistribute along the line — compare that to np.arange(0, 10, 2), which is fixed once you pick a step size:
Top row: np.linspace(0, 10, num) — always includes both endpoints, spacing shrinks as num grows. Bottom row: np.arange(0, 10, 2) — fixed step of 2, stop value excluded, point count never changes.
Random Arrays
Random number generation shows up everywhere in this course: creating practice datasets (like we'll do in a moment), shuffling data, and simulating experiments. Much later, when you build models, randomness will also provide their starting values — but for now, just focus on the three generator functions below.
np.random.seed(42) # Fix the seed for reproducibility
# Uniform random floats in [0, 1)
u = np.random.rand(3, 4) # shape (3, 4)
# Standard normal — bell-curve-shaped values centered on 0
n = np.random.randn(100, 3) # a fake dataset: 100 rows, 3 columns
# Random integers
i = np.random.randint(0, 10, size=(5, 5)) # ints in [0, 10)
# Random choice from an array
arr = np.array([10, 20, 30, 40, 50])
sample = np.random.choice(arr, size=3, replace=False) # [30, 10, 50]
print(np.random.randn(3)) # e.g., [ 0.497 -0.138 0.648]
3 Understanding Array Anatomy
Every NumPy array has four critical properties. In ML debugging, you'll check these constantly — most shape-related errors are immediately diagnosed by reading these values.
arr = np.array([[1, 2, 3],
[4, 5, 6]])
print(arr.shape) # (2, 3) ← 2 rows, 3 columns
print(arr.ndim) # 2 ← 2-dimensional
print(arr.size) # 6 ← 6 total elements
print(arr.dtype) # int64 ← 64-bit integer
In data science, a dataset is usually a table where each row is one sample (one house, one patient, one customer) and each column is one feature (a measured property of that sample: size, age, income). A dataset of 100 houses with 5 measurements each is therefore a 2D array of shape (100, 5). This "rows = samples, columns = features" convention is universal, and we'll use these two words from here on.
Let's look at how these properties change across different array shapes:
| Name | Creation | shape | ndim | size | ML analogy |
|---|---|---|---|---|---|
| Scalar | np.array(5.0) | () | 0 | 1 | A single number (one house price) |
| Vector (1D) | np.array([1,2,3]) | (3,) | 1 | 3 | One sample's features (3 measurements of one house) |
| Matrix (2D) | np.zeros((100, 5)) | (100, 5) | 2 | 500 | 100 samples, 5 features each |
| 3D Tensor | np.zeros((32,28,28)) | (32, 28, 28) | 3 | 25,088 | A stack of 32 grayscale images (28×28 pixels each) |
| 4D Tensor | np.zeros((32,224,224,3)) | (32,224,224,3) | 4 | 4,838,400 | A stack of 32 color images (224×224, 3 color channels) |
A 1D array with shape (3,) is a flat vector. A 2D array with shape (3, 1) is a column vector. These look similar but behave differently in matrix operations. This distinction causes many bugs in ML code. Always verify your shapes with print(arr.shape) when something looks wrong.
Here is the same progression — vector, matrix, and 3D tensor — drawn as grids of cells instead of a table of numbers. Notice how each extra axis adds a new "direction" you can index into:
axis 0 (rows) ↓
axis 1 (rows) ↓ axis 2 (cols) →
Each new axis is a new way to slice the data: a vector has one direction to index, a matrix has rows and columns, and a 3D tensor is a stack of matrices — exactly how np.zeros((32, 28, 28)) represents 32 separate 28×28 images.
4 Data Types (dtype)
Every NumPy array has a single data type (dtype) for all its elements. Choosing the right dtype matters for two practical reasons: it controls how much memory your data occupies, and it determines how precise your calculations are.
# NumPy infers dtype from input
a = np.array([1, 2, 3]) # int64 (default int on 64-bit systems)
b = np.array([1.0, 2.0, 3.0]) # float64
c = np.array([True, False, True]) # bool
print(a.dtype) # int64
print(b.dtype) # float64
print(c.dtype) # bool
# Force a specific dtype
f32 = np.array([1, 2, 3], dtype=np.float32)
print(f32.dtype) # float32
# Cast an existing array to a different dtype
d = a.astype(np.float32)
print(d.dtype) # float32
Why dtype matters
| dtype | Bits | Range / Precision | When to use |
|---|---|---|---|
| float64 | 64 | ±1.8×10³⁰⁸, ~15 sig. digits | Default for NumPy, scientific computing |
| float32 | 32 | ±3.4×10³⁸, ~7 sig. digits | ⭐ Half the memory of float64, precise enough for most ML |
| float16 | 16 | ±65504, ~3 sig. digits | Squeezing memory even further (advanced; you'll meet this in Phase 4) |
| int64 | 64 | −9.2×10¹⁸ to +9.2×10¹⁸ | Indices, class labels |
| bool | 8 | True / False | Boolean masks, binary labels |
Memory is simple arithmetic: a float64 value takes 8 bytes, a float32 takes 4. The same computer can therefore hold twice as many float32 values. Right now, with small arrays, this doesn't matter. But in Phase 4 you'll work with models made of billions of numbers — and there, halving memory decides whether your hardware can run the model at all. That's why the deep learning tools you'll meet later default to float32.
5 Indexing and Slicing
Accessing specific elements or subsets of arrays is a skill you'll use constantly — for extracting features, splitting datasets, debugging, and building custom training loops.
1D Indexing
arr = np.array([10, 20, 30, 40, 50])
arr[0] # 10 — first element
arr[-1] # 50 — last element
arr[1:4] # [20, 30, 40] — slice [start:stop) exclusive stop
arr[::2] # [10, 30, 50] — every other element
arr[::-1] # [50, 40, 30, 20, 10] — reversed
2D Indexing
arr = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
arr[0] # [1, 2, 3, 4] — first row (entire row)
arr[0, 2] # 3 — row 0, column 2
arr[:, 1] # [2, 6, 10] — all rows, column 1 (a column vector)
arr[1:, 2:] # [[7, 8], [11, 12]] — sub-matrix: rows 1–end, cols 2–end
arr[0, :] # [1, 2, 3, 4] — equivalent to arr[0]
Slice notation like arr[1:, 2:] reads awkwardly the first few times. Here is that exact slice applied to the 3×4 array above — the highlighted cells are what gets selected:
Rows are indexed 0–2 top to bottom, columns 0–3 left to right. 1: keeps row 1 onward (skips row 0); 2: keeps column 2 onward (skips columns 0–1). The intersection — the 4 highlighted cells — is the result: [[7, 8], [11, 12]].
Boolean Masking
One of the most powerful and frequently used features. A boolean mask is an array of True/False values — when you use it to index an array, you get back only the elements where the mask is True.
scores = np.array([72, 85, 91, 60, 78, 95, 55])
# Boolean mask: which scores are passing (>= 70)?
mask = scores >= 70
print(mask) # [ True True True False True True False]
# Apply mask to get only the passing scores
passing = scores[mask]
print(passing) # [72 85 91 78 95]
# Or do it in one line:
failing = scores[scores < 70]
print(failing) # [60 55]
# Real-world: keep only the rows of a dataset that pass a condition
data = np.random.randn(1000, 5) # 1000 samples, 5 features
keep = data[:, 0] > 0 # mask: sample's first feature is positive
filtered = data[keep] # fewer than 1000 rows, same 5 columns
print(filtered.shape) # (approx 500, 5)
Here is the scores example from above laid out as a grid: each cell is one score, colored by whether the boolean mask scores >= 70 keeps it or not.
The 5 highlighted (blue) cells are where the mask is True — those are exactly the values scores[mask] returns: [72, 85, 91, 78, 95]. The 2 dimmed cells (60, 55) are dropped because they fail >= 70.
Reshaping Arrays
Reshaping rearranges the same elements into a different shape — no values change, only the way they're organized. You'll use it constantly, because different tools expect data in different shapes.
flat = np.arange(12) # [0, 1, 2, ..., 11] shape: (12,)
# Reshape to 3 rows x 4 columns
matrix = flat.reshape(3, 4)
print(matrix.shape) # (3, 4)
# Use -1 to let NumPy infer one dimension
col_vec = flat.reshape(-1, 1) # shape: (12, 1) — column vector
row_vec = flat.reshape(1, -1) # shape: (1, 12) — row vector
# Real-world: flatten a 28x28 image into one long row of 784 numbers
# (a digital image is just a grid of pixel values — see the spotlight below)
img = np.random.rand(28, 28) # one 28x28 grayscale image
flat_img = img.reshape(-1) # shape: (784,) — all pixels in one row
stack = np.random.rand(32, 28, 28) # a stack of 32 such images
flat_stack = stack.reshape(32, -1) # shape: (32, 784) — one row per image
Real-World Spotlight: How Images Are Arrays in Computer Vision
At the heart of every image classification model (Instagram filters, medical X-ray analysis, autonomous driving) is a simple fact: a digital image is a NumPy array.
- A grayscale image of 256×256 pixels is a 2D array with shape
(256, 256). Each element is an integer 0–255 representing brightness. - An RGB color image at the same size has shape
(256, 256, 3)— width, height, and 3 color channels (Red, Green, Blue). - A stack of 32 such images has shape
(32, 256, 256, 3). This is exactly the 4D tensor that image-recognition models receive as input — you'll build those models yourself in Phase 4.
import numpy as np
# Simulate a single RGB image (normally loaded from disk)
rgb_image = np.random.randint(0, 256, size=(256, 256, 3), dtype=np.uint8)
print(f"Image shape: {rgb_image.shape}") # (256, 256, 3)
print(f"Image dtype: {rgb_image.dtype}") # uint8
print(f"Total pixels: {rgb_image.size}") # 196608
# Normalize: scale pixel values from [0, 255] to [0.0, 1.0]
# Almost every ML method works better on small, consistent values —
# you'll see this "normalization" step in every project in this course
normalized = rgb_image.astype(np.float32) / 255.0
print(f"After normalize: min={normalized.min():.2f}, max={normalized.max():.2f}")
# min=0.00, max=1.00
# Convert to grayscale: average the 3 color channels
grayscale = normalized.mean(axis=2) # axis=2 collapses the channel dimension
print(f"Grayscale shape: {grayscale.shape}") # (256, 256)
# Extract the red channel
red_channel = rgb_image[:, :, 0] # all rows, all columns, first channel
print(f"Red channel shape: {red_channel.shape}") # (256, 256)
# Stack 32 copies into one 4D array (normally these would be 32
# different photos loaded from disk)
batch = np.stack([rgb_image] * 32) # shape: (32, 256, 256, 3)
print(f"Batch shape: {batch.shape}")
Everything you just did — checking shapes, converting dtypes, normalizing values, stacking arrays — is exactly what happens to every photo before a professional image-recognition system sees it. By the time you reach Phase 4 and build such systems, this preparation work will already be second nature.
✍️ Practice Exercise
Test your understanding by completing these tasks in a Python environment (Jupyter, VS Code, or Google Colab):
- Create a 4×4 matrix of zeros. Replace the diagonal with the values 1, 2, 3, 4. Print the result.
- Generate an array of 50 evenly spaced values from −π to +π using
np.linspace. Print the first 5 and last 5 values. - Create a 100×3 array of random standard-normal values. Select all rows where the first column is greater than 1.0. Print the shape of the result.
- Create a 1D array of integers 0–23 and reshape it into a (2, 3, 4) 3D array. What does each dimension represent if this were a batch of 2 grayscale images, each 3×4 pixels?
▶ Show Solution
import numpy as np
# Task 1: 4x4 zeros with diagonal 1,2,3,4
mat = np.zeros((4, 4))
np.fill_diagonal(mat, [1, 2, 3, 4])
print(mat)
# [[1. 0. 0. 0.]
# [0. 2. 0. 0.]
# [0. 0. 3. 0.]
# [0. 0. 0. 4.]]
# Task 2: 50 values from -pi to +pi
import math
x = np.linspace(-math.pi, math.pi, 50)
print(x[:5]) # [-3.142 -2.9 -2.668 -2.427 -2.195]
print(x[-5:]) # [ 2.195 2.427 2.668 2.9 3.142]
# Task 3: filter by first column
data = np.random.randn(100, 3)
filtered = data[data[:, 0] > 1.0]
print(filtered.shape) # roughly (16, 3) — ~16% of normal dist. is > 1
# Task 4: reshape 0-23 into (2, 3, 4)
arr = np.arange(24).reshape(2, 3, 4)
print(arr.shape) # (2, 3, 4)
# Dim 0 (size 2): batch of 2 images
# Dim 1 (size 3): height — 3 pixel rows per image
# Dim 2 (size 4): width — 4 pixel columns per image
📚 Primary Source for This Lesson
NumPy Quickstart Tutorial — official NumPy documentation
The canonical reference for everything covered in this lesson. After completing this lesson, read through it to reinforce the concepts and catch any edge cases you want to explore further. Highly trustworthy — written by the NumPy core team.