NumPy Broadcasting
Broadcasting is how NumPy does maths on arrays of different shapes. When one array is smaller, NumPy behaves as if it were repeated ("stretched") to match the bigger one, but without actually copying any data. You already used it every time you wrote prices * 2: the single number 2 was broadcast to every element.
Picture a teacher adding 5 grace marks to every student. The teacher does not write "5" 30 times; they say it once and it applies to everyone. That is broadcasting.
The rule
NumPy compares the two shapes from the right, one size at a time. Two sizes fit when:
- they are equal, or
- one of them is 1 (it gets stretched).
A missing size on the left counts as 1. If any pair does not fit, you get the error "operands could not be broadcast together".
| Shape A | Shape B | Result | Why |
|---|---|---|---|
(3, 4) | (4,) | (3, 4) | 4 = 4; B's missing size counts as 1 |
(3, 4) | (3, 1) | (3, 4) | 1 stretches to 4; 3 = 3 |
(3, 4) | (3,) | error | 4 and 3 are different and neither is 1 |
How to run the examples
Create a new file named broadcasting.py in your NumpyTutorials folder (see the setup in lesson 01). Any name works; this one just matches the topic. Start the file with these lines:
import numpy as np
Then copy the examples of this page into the same file, one after another, in the order shown, and run it with python broadcasting.py (with your virtual environment active). Some examples use variables created in earlier ones, so keep everything in the one file. Each Output box shows what that example prints.
1. Array and a scalar
prices = np.array([[100, 200, 300],
[400, 500, 600]])
print("prices + 10:\n", prices + 10) # 10 is "stretched" to every element
The simplest case: a single number fits every shape.
2. 2D array and a row
# 3 days x 4 products, and one discount per product
sales = np.array([[10, 20, 30, 40],
[15, 25, 35, 45],
[12, 22, 32, 42]])
price_per_product = np.array([5, 10, 2, 8]) # shape (4,)
print("sales shape", sales.shape, "+ prices shape", price_per_product.shape)
print("revenue = sales * price (each COLUMN gets its price):\n", sales * price_per_product)
sales is (3, 4): 3 days x 4 products. price_per_product is (4,): one price per product. Compared from the right, 4 = 4, and the missing size counts as 1, so the prices are used for every row. Each column is multiplied by its own price.
3. 2D array and a column
bonus_per_day = np.array([[1],
[2],
[3]]) # shape (3, 1)
print("sales + bonus (each ROW gets its bonus):\n", sales + bonus_per_day)
bonus_per_day has shape (3, 1), a column. The 3 matches the rows, and the 1 is stretched across the 4 columns. So each row gets its own bonus: day 0 gets +1, day 1 gets +2, day 2 gets +3.
4. Column x row = table
numbers = np.arange(1, 6)
table = numbers[:, np.newaxis] * numbers # (5, 1) * (5,) -> (5, 5)
print("multiplication table:\n", table)
numbers[:, np.newaxis] is a column (5, 1), and numbers is a row (5,). Both get stretched, the column across and the row down, giving a (5, 5) result: a complete multiplication table in one line.
5. Which shapes fit?
np.broadcast_shapes applies the rule without doing any maths. It is a quick way to check shapes before you calculate.
pairs = [((3, 4), (4,)), ((3, 4), (3, 1)), ((5, 1, 4), (3, 1)), ((2, 3), (3, 2)), ((3, 4), (3,))]
for left, right in pairs:
try:
print(f"{str(left):<10} + {str(right):<7} -> {np.broadcast_shapes(left, right)}")
except ValueError:
print(f"{str(left):<10} + {str(right):<7} -> does NOT fit")
Look at (5, 1, 4) with (3, 1): from the right, 4 and 1 fit (giving 4), 1 and 3 fit (giving 3), and 5 with nothing fits (giving 5). The result is (5, 3, 4).
6. Fixing a shape that does not fit
daily_target = np.array([50, 60, 70]) # one per DAY (row), shape (3,)
try:
sales - daily_target # (3, 4) - (3,) -> no
except ValueError as error:
print("Error:", error)
print("make it a column with [:, np.newaxis]:\n", sales - daily_target[:, np.newaxis])
We have one target per day (row), shape (3,). NumPy compares 3 with the last size of sales, which is 4 (products), so it fails. NumPy always lines shapes up from the right; it does not guess that you meant rows. The fix is to make the targets a column with [:, np.newaxis], shape (3, 1).
When you see "operands could not be broadcast together", print both shapes, write them right-aligned under each other, and you will see which size does not fit. Usually an np.newaxis or keepdims=True fixes it.
7. Broadcasting does not copy data
row = np.array([1, 2, 3])
stretched = np.broadcast_to(row, (4, 3))
print(stretched)
print("strides:", stretched.strides, "(0 = the same row is reused, no copy)")
np.broadcast_to shows what the stretched array "looks like". Its strides (how many bytes to jump to the next row) is 0: every "row" is the same memory, read again. That is why broadcasting is fast and uses no extra memory. Lesson 16 explains strides.
8. Real example
# Share of each product in a day's sales (each row adds up to 100%)
day_totals = sales.sum(axis=1, keepdims=True) # shape (3, 1)
print("share of the day in %:\n", (sales / day_totals * 100).round(1))
# Standardise each column: (value - column mean) / column std
heights_weights = np.array([[160, 55], [172, 70], [168, 65], [181, 80]], dtype=float)
z = (heights_weights - heights_weights.mean(axis=0)) / heights_weights.std(axis=0)
print("standardised (z-scores):\n", z.round(2))
# Distance of each shop from each warehouse (points on a map)
shops = np.array([[0, 0], [3, 4], [6, 8]]) # (3, 2)
warehouses = np.array([[0, 0], [5, 5]]) # (2, 2)
difference = shops[:, np.newaxis, :] - warehouses # (3, 1, 2) - (2, 2) -> (3, 2, 2)
distance = np.sqrt((difference ** 2).sum(axis=2))
print("distance (rows = shops, columns = warehouses):\n", distance.round(2))
- Percentages:
sum(axis=1, keepdims=True)keeps the day totals as a column(3, 1), so each row is divided by its own total, and each row adds up to 100%. - Standardising: subtracting the column means
(2,)and dividing by the column standard deviations works on every row. The result (z-scores) shows how far each person is from the average, in standard deviations. - Distances: shops
(3, 1, 2)minus warehouses(2, 2)gives every shop-warehouse pair(3, 2, 2)at once. Then Pythagoras gives a 3x2 distance table, with no loop at all.
Exercises
Try these exercises on your own before moving on:
- Add np.array([1, 2, 3]) to every row of np.zeros((4, 3)).
- Add np.array([10, 20, 30, 40]) to every COLUMN of np.zeros((4, 3)).
- Make an addition table of 1..9 with broadcasting.
- Will (4, 1, 3) and (2, 1) broadcast? Check with np.broadcast_shapes.
- Each row of marks has a different maximum: divide each row by its own max.
Summary
- Broadcasting lets arrays of different shapes work together by stretching sizes of 1.
- Compare shapes from the right: sizes fit when they are equal or one of them is 1.
- A row
(n,)applies to every row; a column(m, 1)applies to every column. - Fix shape errors with
np.newaxisorkeepdims=True; check withnp.broadcast_shapes. - Broadcasting never copies data, so it is fast and memory-friendly.