NumPy Guide: Arrays, Broadcasting and Vectorization
What vectorisation actually means in NumPy, the one broadcasting rule behind every shape error, and the three bugs it causes in real data code β silent wrong-axis maths, exploding intermediate arrays, and edits that leak through views.
GloryTecks Team
Training & Placement Faculty
Last updated: 24 September 2026
Most slow NumPy code is Python code that happens to use arrays: a for loop over rows, one element at a time. Most confusing NumPy errors come from one rule about shapes. This guide covers both. It explains what vectorisation means, states the broadcasting rule precisely, and then works through the three bugs that rule causes in real data code.
What vectorisation means
The NumPy documentation defines vectorisation as "the absence of any explicit looping, indexing, etc., in the code". The loops still happen, but in pre-compiled C inside NumPy instead of in the Python interpreter (NumPy: what is NumPy?). You describe the operation on whole arrays, and NumPy runs the loop.
import numpy as np
prices = np.array([120.0, 80.5, 99.9, 45.0])
qty = np.array([3, 10, 1, 7])
# Loop version: Python executes every iteration
revenue = np.empty(len(prices))
for i in range(len(prices)):
revenue[i] = prices[i] * qty[i]
# Vectorised version: one expression, the loop runs inside NumPy
revenue = prices * qty # array([360. , 805. , 99.9, 315. ])
total = revenue.sum() # 1579.9On four elements you will not notice the difference. On millions you will. Measure it yourself with %timeit in Jupyter rather than trusting a quoted speed-up, since the gap depends on the operation and the array size.
np.vectorize is not vectorisation
np.vectorize wraps a Python function so it accepts arrays, but the documentation is explicit that it is "provided primarily for convenience, not for performance" and that "the implementation is essentially a for loop" (NumPy: numpy.vectorize). It tidies code; it does not speed it up.
The broadcasting rule
Broadcasting is how NumPy combines arrays of different shapes in one operation. There is exactly one rule (NumPy: broadcasting):
- Write the two shapes right-aligned and compare them from the last dimension, moving left.
- Two dimensions are compatible when they are equal, or when one of them is 1. A size-1 dimension is stretched to match the other.
- If one array has fewer dimensions, the missing leading dimensions count as 1.
- If any pair fails, NumPy raises
ValueError: operands could not be broadcast together.
| Shape A | Shape B | Result | Why |
|---|---|---|---|
| (5, 4) | () β a scalar | (5, 4) | The scalar is stretched everywhere |
| (5, 4) | (4,) | (5, 4) | Last dims equal (4 = 4); Bβs missing first dim counts as 1 |
| (5, 4) | (5,) | ValueError | Last dims 4 and 5: not equal, neither is 1 |
| (5, 4) | (5, 1) | (5, 4) | 4 vs 1 β stretch; 5 = 5 |
| (15, 3, 5) | (15, 1, 5) | (15, 3, 5) | Middle 1 is stretched to 3 |
| (256, 256, 3) | (3,) | (256, 256, 3) | Scale each colour channel of an RGB image |
The stretching is conceptual. NumPy does not copy the smaller array to the larger shape; it reuses the same values, which is why broadcasting is memory-efficient in the common cases.
Broadcasting in real data code
Take a feature matrix X with one row per sample and one column per feature, shape (n_samples, n_features). Standardising each column is one line, because the per-column statistics have shape (n_features,) and line up with the last axis:
rng = np.random.default_rng(0)
X = rng.normal(loc=50, scale=10, size=(1000, 3)) # shape (1000, 3)
mu = X.mean(axis=0) # shape (3,)
sigma = X.std(axis=0) # shape (3,)
Z = (X - mu) / sigma # (1000, 3) with (3,) β (1000, 3)Bug 1: the silent wrong axis
Now normalise each row to sum to 1. The row sums have shape (1000,), which does not line up with the last axis (3), so NumPy raises an error. That is the helpful case. Keep the reduced axis with keepdims=True and the shapes line up:
row_sums = X.sum(axis=1) # shape (1000,)
# X / row_sums # ValueError: (1000,3) vs (1000,)
row_sums = X.sum(axis=1, keepdims=True) # shape (1000, 1)
P = X / row_sums # (1000, 3) with (1000, 1) β (1000, 3)The dangerous case is a square array. With a 3 Γ 3 matrix, M / M.sum(axis=1) does not fail. The (3,) row sums line up with the last axis and each column is divided by a rowβs sum. You get a result of the right shape and the wrong values, with no error. Use keepdims=True for every reduction you intend to broadcast back, and the square case is correct too.
M = np.array([[1., 1., 2.],
[3., 3., 6.],
[0., 5., 5.]])
wrong = M / M.sum(axis=1) # runs, but divides column j by row j's sum
right = M / M.sum(axis=1, keepdims=True) # each row now sums to 1
right.sum(axis=1) # array([1., 1., 1.])Bug 2: the intermediate that does not fit in memory
np.newaxis (the same as None in an index) inserts a size-1 dimension, which lets you broadcast every row of one array against every row of another. That is exactly what a pairwise distance needs:
A = rng.random((500, 64)) # 500 points, 64 features
B = rng.random((800, 64)) # 800 points
diff = A[:, np.newaxis, :] - B[np.newaxis, :, :] # (500, 1, 64) - (1, 800, 64) β (500, 800, 64)
dist = np.sqrt((diff ** 2).sum(axis=-1)) # (500, 800)It is elegant, and it builds a (500, 800, 64) intermediate: 25.6 million float64 values, about 205 MB. Scale both inputs to 10,000 points and the intermediate becomes 6.4 billion values, about 51 GB, before the sum ever runs. The NumPy documentation names this case: broadcasting can use "unnecessarily large amounts of memory", and then it is better to write the outer loop in Python (NumPy: broadcasting). Process A in chunks of rows, or use a routine built for the job, such as SciPy's cdist.
Bug 3: edits that leak through views
Slicing a NumPy array (basic indexing) returns a view: a new array object that shares the original's memory. Indexing with a list or a boolean mask (advanced indexing) always returns a copy (NumPy: copies and views). Writing to a view changes the original:
data = np.arange(10)
first_half = data[:5] # view
first_half[:] = 0
data # array([0, 0, 0, 0, 0, 5, 6, 7, 8, 9]) β original changed
picked = data[[5, 6]] # advanced indexing β copy
picked[:] = -1
data[5:7] # array([5, 6]) β original untouched
safe = data[:5].copy() # take an explicit copy when you mean to modifyIn a pipeline, this shows up as a "cleaning" step that quietly modifies the raw array another step still needs. When a function modifies an array it received, copy it first, or document that it works in place.
When you cannot vectorise
Vectorisation needs each output to be computable independently, or through a standard reduction or accumulation. Ufuncs provide reduce and accumulate, and reductions take an axis argument (NumPy: ufunc basics). A running total that resets whenever a condition holds, or any step that depends on the previous output in a custom way, is inherently sequential. Keep that loop, make it as small as possible, and vectorise everything around it.
Why does NumPy compare shapes from the right?
Does broadcasting copy the smaller array?
How do I check whether an array is a view?
base attribute set to the array it came from. NumPy notes that base tells you whether an array is a view or a copy of another array, not whether it is otherwise new.Should I learn NumPy before pandas?
apply is slow.Where to go next
Apply the same thinking to tables in the pandas guide to DataFrames, cleaning and aggregation. Test yourself with NumPy interview questions for data roles. NumPy and pandas are covered in the data-handling module of our Python programming course.
Sources
- Broadcasting β NumPy user guide
- What is NumPy? β NumPy user guide
- Copies and views β NumPy user guide
- numpy.vectorize β NumPy API reference
- Universal functions (ufunc) basics β NumPy user guide
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Python?
Join GloryTecksβ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.
Pandas Guide: DataFrames, Cleaning and Aggregation
NextPython Projects for Beginners: 15 Ideas to Build
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.