On this page
NumPy broadcasting is the set of rules that lets arithmetic work on arrays of different shapes, so you can write arr + 10 instead of a loop. NumPy compares shapes from the right, and each pair of dimensions must be equal or contain a 1. Those two rules cover the whole system.
The danger is that broadcasting can succeed when you wanted it to fail. Add a (3,) array to a (3, 1) array and you get a 3x3 matrix, not an error. This part of the series is for readers who know indexing and slicing from Part 2. It works through the rules with shape diagrams, the silent bug, the tools that control alignment, and two worked examples. Outputs come from NumPy 2.2.
What are the NumPy broadcasting rules?
When NumPy operates on two arrays, it compares their shapes element by element, starting from the trailing (rightmost) dimension. According to the official broadcasting guide, two dimensions are compatible when:
- they are equal, or
- one of them is 1
If a dimension is missing from the shorter shape, it is treated as 1. If any pair fails both tests, then the operation raises ValueError: operands could not be broadcast together.
Worked through:
A (3, 4)
B (4,)
-----------------
(3, 4) ✓ B is treated as (1,4), stretched down 3 rows
A (3, 1)
B (4,)
-----------------
(3, 4) ✓ A stretched across, B stretched down
A (3, 4)
B (3,)
-----------------
error ✗ 4 vs 3 in the trailing dimension
You can also ask NumPy to apply the rules without doing any arithmetic:
import numpy as np
np.broadcast_shapes((3, 1), (4,)) # (3, 4)
np.broadcast_shapes((8, 1, 6, 1), (7, 1, 5)) # (8, 7, 6, 5)
How does broadcasting work in practice?
In practice, a scalar, a row or a column is stretched across a matrix so one operation applies everywhere. Scalar with array is the simplest broadcast:
a = np.arange(12).reshape(3, 4)
a + 100 # every element gets 100 added
a * 2
Next, a row vector across a matrix:
matrix = np.array([[1, 2, 3],
[4, 5, 6]]) # (2, 3)
row = np.array([10, 20, 30]) # (3,)
matrix + row
# [[11 22 33]
# [14 25 36]]
The row is applied to each row of the matrix. This is the pattern behind per-column operations such as centring:
data = np.array([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]])
col_means = data.mean(axis=0) # [4. 5. 6.], shape (3,)
centred = data - col_means # each column gets its own mean subtracted
A column vector down a matrix, by contrast, needs an explicit second dimension:
col = np.array([[10],
[20]]) # (2, 1)
matrix + col
# [[11 12 13]
# [24 25 26]]
Why does NumPy broadcasting fail silently?
Because shapes can be compatible in a way you did not intend. An (n,) array and an (n, 1) array both stretch, so NumPy returns an (n, n) matrix instead of raising an error, and nothing warns you.
a = np.array([1, 2, 3]) # shape (3,)
b = np.array([[10], [20], [30]]) # shape (3, 1)
a + b
# [[11 12 13]
# [21 22 23]
# [31 32 33]]
You expected [11 22 33]. Instead, you got a 3x3 matrix.
Right-aligning the shapes, (3,) becomes (1, 3) and meets (3, 1). Both dimensions have a 1, so both stretch, and the result is the outer sum.
Nothing errors. Downstream you get wrong numbers, or a memory explosion when the arrays are large: a (10000, 1) with a (10000,) produces a hundred-million-element array.
The fix is always to check shapes:
print(a.shape, b.shape) # (3,) (3, 1)
a + b.ravel() # [11 22 33] - flatten b to (3,)
The row-wise version of this bug raises an error instead, which is the lucky case:
M = np.array([[1., 2., 3.],
[4., 5., 6.]])
M.mean(axis=1) # [2. 5.], shape (2,)
M - M.mean(axis=1, keepdims=True) # [[-1. 0. 1.] [-1. 0. 1.]]
Without keepdims=True, M - M.mean(axis=1) fails with shapes (2,3) and (2,), because the trailing dimensions 3 and 2 do not match.
How do you control broadcasting with np.newaxis?
Add a length-one dimension exactly where you want the stretch to happen, with np.newaxis, reshape or keepdims=True. The NumPy indexing guide describes it as a way to insert a new axis of length one, and it is simply an alias for None:
a = np.array([1, 2, 3])
a[:, np.newaxis].shape # (3, 1) - column
a[np.newaxis, :].shape # (1, 3) - row
a.reshape(-1, 1).shape # (3, 1) - same as the first
This makes an outer operation explicit rather than accidental:
x = np.array([1, 2, 3])
y = np.array([10, 20])
x[np.newaxis, :] * y[:, np.newaxis]
# [[10 20 30]
# [20 40 60]]
Written that way, the intent is visible to whoever reads it next.
Worked example: normalising features
Broadcasting makes standard preprocessing a two-line operation:
rng = np.random.default_rng(0)
X = rng.normal(size=(100, 5))
means = X.mean(axis=0) # (5,)
stds = X.std(axis=0) # (5,)
X_scaled = (X - means) / stds
X_scaled.std(axis=0).round(10) # [1. 1. 1. 1. 1.]
Each of the 5 columns gets its own mean and standard deviation, applied down all 100 rows, with no loop and no copy of the statistics.
The axis argument is the other half of this. axis=0 collapses rows, giving one value per column, while axis=1 collapses columns, giving one value per row. Part 4 covers that argument in detail.
Worked example: pairwise distances without a loop
Adding a dimension to each copy of a point array turns every pair into one subtraction:
pts = np.array([[0, 0], [3, 4], [6, 8]]) # (3, 2)
diff = pts[:, np.newaxis, :] - pts[np.newaxis, :, :] # (3, 3, 2)
dist = np.sqrt((diff ** 2).sum(axis=-1))
# [[ 0. 5. 10.]
# [ 5. 0. 5.]
# [10. 5. 0.]]
The trade-off is memory. For n points the intermediate array is n × n × 2, so the NumPy documentation itself notes that broadcasting can be a bad idea when it creates a very large intermediate array. For tens of thousands of points, process in chunks instead.
NumPy broadcasting quick reference
| Shapes | Result | Why |
|---|---|---|
(3, 4) and scalar |
(3, 4) |
scalar stretches everywhere |
(3, 4) and (4,) |
(3, 4) |
row applied to each row |
(3, 4) and (3, 1) |
(3, 4) |
column applied to each column |
(3, 1) and (4,) |
(3, 4) |
both stretch: an outer operation |
(3, 4) and (3,) |
error | trailing 4 vs 3 |
(2, 3) and (2,) |
error | trailing 3 vs 2; use keepdims=True |
Common NumPy broadcasting mistakes
- Mixing
(n,)and(n, 1). The sum becomes an(n, n)matrix; check.shapefirst. - Dropping
keepdims=Trueon row-wise statistics. The result no longer lines up with the rows. - Broadcasting in place into a smaller array.
z = np.zeros(3); z += np.ones((3, 3))raises "non-broadcastable output operand", because the result cannot fit inz. - Building huge intermediates. Pairwise tricks on large inputs can use more memory than a chunked loop.
The Sunday Growth Brief
One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.
No spam. Unsubscribe in one click.
Related reading
NumPy Part 2: indexing and slicing precedes this, and NumPy Part 1 starts the series. NumPy Part 4: aggregation and linear algebra continues. For the operators themselves, see Python operators.
Frequently asked questions
What is broadcasting in NumPy?
Broadcasting is the set of rules that lets NumPy apply an arithmetic operation to arrays of different shapes. Shapes are compared from the trailing dimension, and any dimension of size 1 is virtually stretched to match the other array. No data is copied for the stretched operand, so adding a scalar or a row vector to a large matrix is both short to write and fast to run.
Why did adding two NumPy vectors give me a 2D result?
Almost certainly one was shaped (n, 1) and the other (n,). Right-aligned, those become (n, 1) and (1, n), and both stretch, so the result is an (n, n) table of every pairwise sum. Print .shape on both arrays before the operation, then flatten the column with ravel() or reshape the other array so the shapes match.
Does broadcasting use extra memory?
The stretched operand does not: NumPy steps through it with a stride of zero along the broadcast dimension instead of copying it. The result array is still allocated at full size, however. That is why an accidental (10000, 1) plus (10000,) operation can exhaust memory, because it creates a result with a hundred million elements.
How do I add a dimension to a NumPy array?
Use np.newaxis, which is simply an alias for None, inside the index. For a one-dimensional array of shape (3,), arr[:, np.newaxis] gives a (3, 1) column and arr[np.newaxis, :] gives a (1, 3) row. arr.reshape(-1, 1) produces the same column, and np.expand_dims(arr, axis=1) is a more explicit alternative if you prefer named functions.
Written by
Vanshika Nigam
ETL & Data Engineer, Accenture
ETL developer at Accenture working with Informatica, Snowflake and DBT, specialising in data mapping, cleansing and pipeline performance.

