Vectorisation and ufuncs
The loop doesn’t disappear — it moves somewhere much faster.
elementwise opsnp.sqrtnp.wherenp.maximum+=out=Watch it happen
Play it through, or step back and forth yourself.
out = []
for value in x:
out.append(value * 2)To double every value in a list, Python makes you say so: a loop, one element at a time, with the interpreter doing bookkeeping on every pass. It works. It's just slow, and it buries the intent.
The idea
The single biggest shift from ordinary Python to NumPy is that you stop writing loops. Not because loops are forbidden, but because writing one over an array usually means you've missed the operation that already does it.
out = [] # the Python way
for value in x:
out.append(value * 2)
x * 2 # the NumPy wayThe second version isn't just shorter — it's a different amount of work. In the loop, Python creates an integer object per element, does a method call per element, and grows a list. In x * 2, NumPy hands a contiguous block to compiled C, which walks it with no Python involvement at all. Typically 10-100x faster, and the gap widens with size.
The loop is still there. It just isn't in your program any more.

Ufuncs
Functions that work element by element are universal functions, or ufuncs. Arithmetic operators are ufuncs. So are np.sqrt, np.exp, np.log, np.sin, np.abs, np.round. All of them broadcast, so everything from the last lesson still applies.
Some take two arrays: np.maximum(x, y) compares element by element, which is different from np.max(x), which reduces one array to a single value. Similar names, unrelated jobs.

Watch the dtype
Vectorised arithmetic respects the array's dtype, and integer arrays overflow silently rather than growing:
np.array([200], dtype=np.uint8) + 100 # 44 — wrapped around
np.array([1, 2, 3]) / 2 # float64, division always isThis matters most with images, which are uint8. Brightening one by adding 60 will wrap the bright pixels to black unless you widen first — you'll do exactly that in the capstone.
In place, when it counts
x = x + 1 allocates a whole new array. x += 1 writes into the existing one. For small arrays it doesn't matter; for large ones it halves your memory traffic. Just remember that in-place writes hit every view onto that memory — the trap from lesson 3.
Practice
Write it yourself. The answer is there when you want it.
Putting the kettle on…
Starting up…
Write it yourself
not gradedWith x = np.array([3, 1, 4, 1, 5, 9]), print x * 2, its square roots, and np.maximum(x, 4). Then print np.max(x) straight after, so the difference between elementwise and reducing is on consecutive lines. Finish by adding 100 to a uint8 holding 200, and reading the answer carefully.
Your turn
3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Square every element of v, without writing a loop.
Take the square root of every element of cups, rounded to 2 decimal places.
Return cups with every value below 100 raised up to exactly 100, leaving the rest alone.
