C.W.K.
Stream
Lesson 01 of 07 · published

NumPy Arrays vs Python Lists — Why ndarray

~13 min · numpy, ndarray, vectorization

Level 0Curious Reader
0 XP0/47 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

The bedrock under everything

NumPy (Numerical Python) is the foundation of nearly all numerical computing in Python. Pandas is built on it. PyTorch and TensorFlow speak it. scikit-learn requires it. Most of PyArrow can hand you arrays compatible with it. If you understand ndarray, you understand the engine room of the modern Python data stack.

Current stable: NumPy 2.4.4 (March 2026). NumPy 2.0 (June 2024) was the breaking-change release that cleaned up legacy APIs; everything from 2.0+ is the modern world.

Why is it fast?

A Python list is a vector of pointers. Each element is a full Python object with its own type tag, reference count, and allocation. Iterating over a million-element list means a million pointer dereferences and a million Python-level method dispatches.

A NumPy ndarray is a single block of contiguous memory holding raw, typed values — no per-element pointers, no type tags. Iterating is a tight C loop with no Python overhead. The same operation that takes seconds on a list takes milliseconds on an array.

What you actually get

  • Fast n-dimensional arrays — homogeneous typed data in contiguous memory.
  • Vectorized operations — element-wise math runs at C speed without Python loops.
  • Broadcasting — arrays of different shapes auto-align for element-wise ops.
  • Universal functions (ufuncs)np.sin, np.exp, np.where, etc., all element-wise and parallelizable.
  • Linear algebra, FFT, random — the numerical toolkit comes in the box.
  • Modern Generator APInp.random.default_rng() replaces the legacy global state.

Code

List vs ndarray — the speed difference is not subtle·python
import numpy as np, time, math

n = 5_000_000
py_list = list(range(n))
np_arr  = np.arange(n)

# Pure Python — element-wise sqrt
t = time.perf_counter()
py_result = [math.sqrt(x) for x in py_list]
print(f'list comprehension: {time.perf_counter() - t:.3f}s')

# NumPy — element-wise sqrt, vectorized
t = time.perf_counter()
np_result = np.sqrt(np_arr)
print(f'np.sqrt:           {time.perf_counter() - t:.3f}s')

# On a recent laptop you'll see ~50–100x. The gap grows with array size.
Modern random Generator API — reproducibility built in·python
import numpy as np

rng = np.random.default_rng(seed=42)        # the modern entry point
data = rng.normal(loc=100, scale=15, size=1_000_000)

# Vectorized stats, no loops
print(f'mean: {data.mean():.2f}')           # ~100.0
print(f'std:  {data.std():.2f}')            # ~15.0

# Boolean masking — also vectorized
above_120 = data[data > 120]
print(f'fraction > 120: {len(above_120) / len(data):.3%}')

# Old style (legacy global state) — still works, but don't use it in new code
# np.random.seed(42); np.random.normal(...)

External links

Exercise

Generate one million normally distributed values with rng.normal(). Compute the mean two ways: a Python for loop with sum() / len(), and arr.mean(). Time both with time.perf_counter(). Write down the ratio. The ratio is the rest of your career as a Python data person.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.