NumPy

Expert · 17 min read · ▶ live playground · ✦ checkpoint

A Python list can hold almost anything, and that generosity is useful. It is also costly when your real goal is "add five points to every score" or "convert thousands of temperatures." NumPy gives Python fast array math, math over ordered blocks of same-kind values: you arrange, index, slice, and transform whole blocks at once.

The ndarray: one object, many numbers

NumPy is a third-party package, code installed outside Python's standard library, that specializes in numerical data. By convention you import it as np, the same way many Python developers write pathlib code with Path instead of spelling the whole module name every time.

The main object is an ndarray, short for "N-dimensional array," which means one array can be a row, a table, or a deeper block of values. An element is one value inside an array. A shape is the array's size in each direction; (4,) means one row-like direction with four elements. A dtype is the exact NumPy value type stored in every slot.

A list is the row of numbered slots you already know. A one-dimensional NumPy array is a stricter row: every slot is cut for the same kind of value, so NumPy can pack the data tightly and run compiled loops over it. Compiled code is code translated closer to machine instructions before it runs; you met that idea back when Python's interpreter model was introduced.

import numpy as np
 
scores = np.array([72, 88, 91, 65], dtype=np.int64)
 
print(scores)
print(type(scores))
print(scores.shape)
print(scores.dtype)

[72 88 91 65]
<class 'numpy.ndarray'>
(4,)
int64

You don't need to memorize int64; read it as "an integer type NumPy can pack tightly." That strictness is the trade: less flexibility than a list, much faster numeric work.

Creating arrays and changing shape

You will usually create arrays from existing Python data, from a range of numbers, or from a starter shape filled with zeros. Reshaping means giving the same elements a new row-and-column layout without inventing or dropping values.

import numpy as np
 
daily_steps = np.array([4200, 5100, 6350, 7000, 4800, 5600], dtype=np.int64)
practice_targets = np.arange(2, 8, 2, dtype=np.int64)
blank_scorecard = np.zeros((2, 3), dtype=np.int64)
step_grid = daily_steps.reshape(2, 3)
 
print("steps:", daily_steps)
print("practice targets:", practice_targets)
print("blank:")
print(blank_scorecard)
print("grid:")
print(step_grid)
print("grid shape:", step_grid.shape)

steps: [4200 5100 6350 7000 4800 5600]
practice targets: [2 4 6]
blank:
[[0 0 0]
[0 0 0]]
grid:
[[4200 5100 6350]
[7000 4800 5600]]
grid shape: (2, 3)

np.array(...) turns a Python collection into an array. np.arange(start, stop, step) works like the range() shape you already know, but it returns a NumPy array. np.zeros((2, 3)) creates two rows and three columns of zeros.

reshape(2, 3) works because six step counts fit exactly into two rows of three. If you asked for reshape(4, 2), NumPy would refuse: six elements cannot fill eight slots.

Indexing and slicing arrays

Array indexing reuses the ruler idea from strings and lists: positions start at 0. A dimension is one direction you can index along, such as rows or columns. In a two-dimensional array, NumPy commonly reads indexes as row first, column second.

import numpy as np
 
scores = np.array([
    [82, 91, 77],
    [90, 85, 88],
    [76, 95, 89],
], dtype=np.int64)
 
print(scores[0, 1])
print(scores[1])
print(scores[:, 2])
print(scores[0:2, 1:3])

91
[90 85 88]
[77 88 89]
[[91 77]
[85 88]]

scores[0, 1] reads row 0, column 1: the first player's second round. scores[1] gives the whole second row. A slice is the same ribbon cut you used on strings and lists; scores[:, 2] means "all rows, column 2." The colon by itself keeps the whole direction.

That comma is the big NumPy difference. With nested Python lists you write scores[0][1]: first fetch a row, then fetch an item from that row. With an ndarray, scores[0, 1] asks the array for the exact cell in one move.

Try it - shape a score sheet

The first time you run a NumPy playground in this lesson, the browser downloads NumPy once. After that, the package stays loaded for this page, and each Run still starts your own code fresh.

Change the raw scores, the reshape size, or the bonus amount. Watch how one expression updates the whole sheet.

python — playgroundlive
⌘/Ctrl + Enter to run

Broadcasting: matching shapes without extra loops

Broadcasting is NumPy's rule for stretching a smaller value pattern across a larger compatible array. Picture one bonus sticker sheet lined up over every row of a score table: NumPy doesn't copy it by hand in your code, but it behaves as if the pattern were repeated where the shapes line up.

import numpy as np
 
scores = np.array([
    [82, 91, 77],
    [90, 85, 88],
    [76, 95, 89],
], dtype=np.int64)
 
round_bonus = np.array([1, 0, 2], dtype=np.int64)
player_penalty = np.array([[0], [3], [1]], dtype=np.int64)
 
print(scores + round_bonus)
print(scores - player_penalty)

[[83 91 79]
[91 85 90]
[77 95 91]]
[[82 91 77]
[87 82 85]
[75 94 88]]

scores has shape (3, 3): three rows and three columns. round_bonus has shape (3,), so NumPy lines it up with the three columns and applies it to every row. player_penalty has shape (3, 1), so NumPy lines it up with the three rows and stretches each one-column penalty across that row.

Broadcasting is powerful because it keeps the idea visible. You write "add these round bonuses to the sheet," not "loop through every row and every column and remember which bonus goes where."

Vectorized operations and ufuncs

A vectorized operation is an array operation written once that NumPy applies across many elements. Instead of writing a for loop that converts one temperature at a time, you describe the column math.

A ufunc, short for universal function, is a NumPy function that applies one operation element by element across arrays. np.abs() handles absolute value across an array. np.maximum() chooses the larger value at each position.

import numpy as np
 
temperatures_c = np.array([18.0, 21.5, 24.0, 19.5])
 
temperatures_f = temperatures_c * 9 / 5 + 32
cooling_needed = np.maximum(temperatures_c - 22, 0)
comfort_distance = np.abs(temperatures_c - 21)
 
print("F:", temperatures_f)
print("cooling:", cooling_needed)
print("comfort distance:", comfort_distance)

F: [64.4 70.7 75.2 67.1]
cooling: [0. 0. 2. 0.]
comfort distance: [3. 0.5 3. 1.5]

The expression temperatures_c * 9 / 5 + 32 reads like ordinary arithmetic, but every operator works across the whole array. np.maximum(temperatures_c - 22, 0) also uses broadcasting: the single 0 is compared with every element.

When the data is numeric and rectangular, reach for this style first. It is shorter, it says what you mean, and it lets NumPy do the heavy work in optimized code.

Checkpoint

Answer all three to mark this lesson complete

NumPy is the base layer for much of Python's data work: fast, rectangular, numeric arrays. Next, pandas adds labels, messy table habits, and the selection tools you want when the data starts looking like real spreadsheets.

+50 XP on completion