When would you use a Python list versus a NumPy array, and what are the performance trade-offs?
Python lists are heterogeneous, pointer-based, and general-purpose. NumPy arrays are homogeneous, stored as contiguous typed memory, and support vectorised operations that run at C speed. For numerical work on more than a few hundred elements, NumPy is almost always faster and more memory-efficient.
How to think about it
In a data or ML interview this question is really probing whether you understand why NumPy is fast, not just that it is. The whole answer is memory layout. A Python list stores an array of pointers to arbitrary objects; a NumPy array stores raw values of one type, packed back to back like a C array.
That difference cascades. A list of a million floats is a million separate float objects scattered across the heap (each with a type tag and a reference count), plus a million pointers to reach them. A NumPy float64 array is a million doubles in one contiguous block — 8 bytes each, no headers, no pointers. Which buys three things: far less memory, cache-friendly access the CPU can prefetch, and operations that dispatch to compiled BLAS/LAPACK kernels entirely outside the interpreter loop.
A worked example
import sys
import numpy as np
# Same 1000 floats, very different footprints
small_list = [1.0] * 1000
small_arr = np.ones(1000, dtype=np.float64)
print(f"list (1000 floats) : {sys.getsizeof(small_list)} bytes (pointers only)")
print(f"numpy(1000 floats) : {small_arr.nbytes} bytes (raw doubles)")
# NumPy gives vectorised math for free — one C call, no Python loop
arr = np.array([1.0, 4.0, 9.0, 16.0])
print("sqrt via numpy :", np.sqrt(arr))
print("arr * 2 + 1 :", arr * 2.0 + 1.0)
list (1000 floats) : 8056 bytes (pointers only)
numpy(1000 floats) : 8000 bytes (raw doubles)
sqrt via numpy : [1. 2. 3. 4.]
arr * 2 + 1 : [ 3. 9. 19. 33.]
There’s a subtle catch in those sizes: the list reports 8,056 bytes, but that’s only the pointer array — sys.getsizeof doesn’t count the thousand actual float objects it points to, each another ~24 bytes. So the list’s real footprint is several times the array’s, not roughly equal. And the last two lines show what that layout buys: np.sqrt(arr) and arr * 2.0 + 1.0 transform the whole array in a single compiled call — no Python loop, the operation NumPy was built for. On a million elements, that vectorised form is routinely tens of times faster than a list comprehension.
When to stick with a plain list
- Heterogeneous elements:
[42, "hello", None, True]. - Append-heavy work where you don’t know the final size up front.
- Small collections, where NumPy’s overhead outweighs the win.
- Non-numeric data — strings, objects, nested dicts.
| Need | Use |
|---|---|
| general-purpose mixed data | list |
| numeric computation / ML features | numpy.ndarray |
| tabular data with named columns | pandas.DataFrame |
| typed compact sequence, stdlib only | array.array |