Performance & Vectorization vs Loops

Reviewed & published by Brayan K

Vectorization is the practice of expressing a computation as operations on whole arrays instead of element-by-element Python loops — letting NumPy run the work as a single compiled C loop over contiguous memory rather than slow, interpreted Python. In this lesson you'll see why Python loops are slow, rewrite a loop as a one-line vectorized expression, preallocate instead of appending, lean on built-in aggregations, and understand why np.vectorize is convenience rather than speed.

Part of the free NumPy course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1 Why Python Loops Are Slow

A Python for loop over numbers pays a tax on every iteration: it interprets bytecode and treats each number as a full Python object that must be unboxed, operated on, and reboxed. A NumPy array keeps raw numbers in contiguous memory and runs the whole operation as one compiled C loop, skipping all that overhead.

import numpy as np

data = np.arange(1, 6)   # [1 2 3 4 5]
# Slow style: an explicit Python loop building a list
squares_loop = []
for x in data:
    squares_loop.append(x ** 2)
print(squares_loop)        # [1, 4, 9, 16, 25]

# Fast style: one vectorized expression, no Python loop
squares_vec = data ** 2
print(squares_vec)         # [ 1  4  9 16 25]

# ✅ Expected output:
# [np.int64(1), np.int64(4), np.int64(9), np.int64(16), np.int64(25)]
# [ 1  4  9 16 25]

Expected output: both produce 1, 4, 9, 16, 25 — identical results, but the vectorized version is far faster on big data.

2 Rewriting a Loop as a Vectorized Expression

Most element-wise loops collapse into a single expression. Whenever you see a loop that does the same arithmetic to each element, look for the array version: a + b, a * 2, or a combined formula applied to whole arrays at once.

import numpy as np

prices   = np.array([10.0, 20.0, 30.0])
quantity = np.array([2, 1, 3])

# Loop version: total cost per line
totals_loop = []
for i in range(len(prices)):
    totals_loop.append(prices[i] * quantity[i] * 1.1)   # +10% tax
print(totals_loop)          # [22.0, 22.0, 99.00000000000001]

# Vectorized version: one expression on whole arrays
totals_vec = prices * quantity * 1.1
print(totals_vec)           # [22. 22. 99.]
print(np.allclose(totals_loop, totals_vec))   # True

# ✅ Expected output:
# [np.float64(22.0), np.float64(22.0), np.float64(99.00000000000001)]
# [22. 22. 99.]
# True

Expected output: both give roughly [22, 22, 99], and np.allclose confirms they match with True.

3 Preallocate Instead of Appending

Calling np.append inside a loop is a trap: each call builds a brand-new array and copies everything, which is quadratic and painfully slow. If you truly need a loop, preallocate with np.zeros(n) once and fill slots by index. Better still, vectorize and skip the loop.

import numpy as np

n = 5
# Bad: append in a loop -> copies the whole array every time
slow = np.array([], dtype=int)
for i in range(n):
    slow = np.append(slow, i * i)
print(slow)                 # [ 0  1  4  9 16]

# Good: preallocate, then fill by index (or skip the loop with np.arange(n)**2)
fast = np.zeros(n, dtype=int)
for i in range(n):
    fast[i] = i * i
print(fast)                 # [ 0  1  4  9 16]

# ✅ Expected output:
# [ 0  1  4  9 16]
# [ 0  1  4  9 16]

Expected output: both print [ 0 1 4 9 16] — same result, but the preallocated version avoids the costly per-step copies that np.append makes.

4 Built-in Aggregations & np.vectorize

Don't hand-roll a running total or maximum in Python. NumPy's built-in aggregations — .sum(), .mean(), .max(), .min() — run in C and are both shorter and dramatically faster than a manual loop.

import numpy as np

data = np.arange(1, 101)   # 1..100

# Manual loop sum
total_loop = 0
for x in data:
    total_loop += x
print(total_loop)          # 5050

# Built-in aggregations — each is one call that runs in C
print(data.sum())          # 5050
print(data.mean())         # 50.5

# ✅ Expected output:
# 5050
# 5050
# 50.5

Expected output: 5050 from both the loop and .sum(), then mean 50.5.

Beware of np.vectorize: it wraps a scalar function so it accepts arrays — handy for readability. But it still loops in Python internally, so it is not a performance tool. For real speed, express the logic with true NumPy operations (arithmetic, np.where, masks).

import numpy as np

arr = np.array([-2, -1, 0, 1, 2])

# Convenience: np.vectorize wraps a scalar function (still loops internally)
def clamp(x):
    return 0 if x < 0 else x
vclamp = np.vectorize(clamp)
print(vclamp(arr))            # [0 0 0 1 2]

# Fast: a true vectorized operation does the same thing in C
print(np.where(arr < 0, 0, arr))   # [0 0 0 1 2]

# ✅ Expected output:
# [0 0 0 1 2]
# [0 0 0 1 2]

Expected output: both print [0 0 0 1 2], but np.where runs in C while np.vectorize only looks fast.

🎯 YOUR TURN: Fill in the Blanks

Replace each ___ so the slow loop becomes a fast, vectorized one-liner that doubles every value and sums the result.

import numpy as np

data = np.array([1, 2, 3, 4])

# Vectorized: double every element with one expression (no loop)
doubled = data * ___          # fill in the multiplier -> [2 4 6 8]
print(doubled)

# Built-in aggregation instead of a manual loop sum
total = doubled.___()         # fill in the method name -> 20
print(total)

Expected output: [2 4 6 8] then 20. (Answers: 2, sum.)

⚠️ Common Errors & Quick Reference

❌ Looping over a huge array element by element

A Python for loop over millions of elements can be 50-100x slower than the array version.

✅ Fix: replace the loop with a single vectorized expression or a built-in aggregation.

❌ Expecting np.vectorize to speed things up

It only changes how the function is called; the work still happens in a Python-level loop.

✅ Fix: rewrite the logic with real NumPy ops like np.where, masks, or arithmetic.

Slow PatternFast Replacement
Loop doing arithmetica * b + c
Manual running totalarr.sum()
np.append in a loopnp.zeros(n) then fill
if/else per elementnp.where(cond, a, b)

🏆 Mini Challenge: Vectorize a Distance Calculation

Compute the Euclidean distance of each point from the origin, first with a loop, then with one vectorized expression, and confirm they match.

import numpy as np

xs = np.array([3.0, 5.0, 8.0])
ys = np.array([4.0, 12.0, 6.0])

# One vectorized expression replaces a per-point loop
dist = np.sqrt(xs**2 + ys**2)
print(dist)                   # [ 5. 13. 10.]

# ✅ Expected output:
# [ 5. 13. 10.]

❓ Frequently Asked Questions

Lesson complete — you write fast NumPy now!

You know why Python loops are slow, how to rewrite them as vectorized expressions, when to preallocate, how to reach for built-in aggregations, and why np.vectorize is convenience rather than speed.

Practice quiz

Why is a NumPy vectorized operation faster than a Python for loop?

  • It runs as one compiled C loop over contiguous memory
  • It uses more RAM
  • It skips some elements
  • It runs on the GPU automatically

Answer: It runs as one compiled C loop over contiguous memory. NumPy runs the whole operation in compiled C over a contiguous block, avoiding per-element Python overhead.

What does 'vectorization' mean?

  • Drawing vectors
  • Expressing a calculation as operations on whole arrays
  • Using a for loop
  • Converting to a list

Answer: Expressing a calculation as operations on whole arrays. Vectorization replaces element-by-element loops with whole-array expressions.

Does np.vectorize make code run faster?

  • Yes, it compiles to C
  • Yes, it uses threads
  • No, it still loops in Python internally
  • Yes, always

Answer: No, it still loops in Python internally. np.vectorize is a convenience wrapper; it still loops in Python and is not a speed tool.

Why is calling np.append inside a loop slow?

  • It uses the GPU
  • It sorts each time
  • It compresses data
  • It copies the whole array on every call, giving quadratic cost

Answer: It copies the whole array on every call, giving quadratic cost. Each np.append builds a new array and copies all existing data, which is quadratic.

Which is the recommended fast alternative to a loop building squares?

  • data ** 2
  • a Python for loop with append
  • np.vectorize(pow)
  • map(lambda x: x*x, data)

Answer: data ** 2. data ** 2 is one vectorized expression that runs in C with no Python loop.

What is the fast way to total an array instead of a manual loop?

  • A while loop
  • arr.sum()
  • np.vectorize(add)
  • reduce in pure Python

Answer: arr.sum(). Built-in aggregations like arr.sum() run in C and are far faster than a manual loop.

Which truly fast operation replaces a per-element if/else?

  • np.vectorize
  • a list comprehension
  • np.where(cond, a, b)
  • a for loop

Answer: np.where(cond, a, b). np.where applies the conditional element-wise in C, the genuinely fast replacement.

If you must use a loop to fill values, what should you do first?

  • Append each value
  • Use np.vectorize
  • Convert to a list
  • Preallocate with np.zeros(n) and fill by index

Answer: Preallocate with np.zeros(n) and fill by index. Preallocating once and filling by index avoids the repeated copies that np.append causes.

How does a Python loop treat each number compared to a NumPy array element?

  • As a boxed Python object that must be unboxed each iteration
  • As raw C memory
  • As a string
  • As a GPU value

Answer: As a boxed Python object that must be unboxed each iteration. Python boxes each number as a full object, adding overhead that NumPy's raw contiguous storage avoids.

Both a loop and a vectorized version of the same math should produce...

  • Different results
  • The same results, but vectorized is faster
  • An error
  • Only integer results

Answer: The same results, but vectorized is faster. They compute identical results; vectorization just runs much faster on large data.

Continue this course

Frequently asked questions

Why is NumPy faster than a Python for loop?

NumPy stores numbers in a single block of contiguous memory and runs each operation as one pre-compiled C loop. A Python for loop, by contrast, interprets bytecode and boxes every element as a full Python object on each iteration, which adds huge overhead. The result is often a 10-100x speed-up on large arrays.

What does 'vectorization' actually mean?

Vectorization means expressing a calculation as operations on whole arrays instead of element-by-element loops. You write one expression like a + b or arr * 2, and NumPy applies it to every element internally at C speed with no Python-level loop.

Does np.vectorize make my code faster?

No. np.vectorize is a convenience wrapper that lets a scalar function accept arrays, but it still loops in Python under the hood, so it is essentially as slow as a manual loop. Use it for readability, not speed — for real performance, rewrite the math using true NumPy operations.

Why should I preallocate instead of appending in a loop?

Repeatedly calling np.append inside a loop creates a brand-new array and copies all existing data every time, giving quadratic cost. Preallocating with np.zeros(n) (or np.empty) once and filling slots by index avoids the copies — and most of the time you can skip the loop entirely with a vectorized expression.