ndarray vs Python List
How a NumPy ndarray differs from a Python list — homogeneous types, contiguous memory, vectorized maths, fixed size — with a full comparison table.
A Python list and a NumPy array can look similar — both hold a sequence of Rohit's scores, both support [0] and [1:4]. But underneath they are built completely differently, and that difference decides speed, memory and what operations mean.
Definition
A Python list is a general-purpose, resizable sequence that stores references (pointers) to Python objects, which may be of any type. A NumPy ndarray is a fixed-size, homogeneous N-dimensional array that stores the raw values themselves, all of the same
dtype, in a contiguous block of memory.
- References / pointers — a list does not hold the number 72 directly; it holds the address of a Python
intobject somewhere else in memory. - Homogeneous — "of the same kind". Every element of an ndarray has exactly the same data type (
dtype), e.g.int64. - Contiguous — elements are stored back-to-back with no gaps, so element
iis at a fixed, predictable position. - Fixed-size — an ndarray's size is set when it is created. There is no
.append()method; "adding" an element creates a new array.
Memory layout — the real reason for the speed difference
text
Python list [72, 45, 88, 31]
list: [ptr] [ptr] [ptr] [ptr]
| | | |
[int] [int] [int] [int] <- each is a full Python object
(type info + ref count + value)
stored wherever memory was free
NumPy array np.array([72, 45, 88, 31])
data: [72][45][88][31] <- raw 8-byte integers, back-to-back,
one shared dtype, no pointersTo add up the list, Python must follow each pointer, check "what type is this object?", unpack the value, add, and create a new object for the result. NumPy just walks along one block of memory with a C loop.
Homogeneous types — NumPy converts everything to ONE type
python
mixed = [1, "Virat", True, 4.5]
print(mixed)
print([type(x).__name__ for x in mixed])
import numpy as np
arr = np.array([1, "Virat", True, 4.5])
print(arr)
print(arr.dtype)
nums = np.array([72, 45.5, True])
print(nums, nums.dtype)text
[1, 'Virat', True, 4.5]
['int', 'str', 'bool', 'float']
['1' 'Virat' 'True' '4.5']
<U32
[72. 45.5 1. ] float64How it works: the list happily keeps four different types. NumPy must choose one type that can hold everything, so it converts all four values to strings (<U32 = Unicode string of up to 32 characters). In the second array, 72, 45.5 and True can all be represented as floats, so the dtype becomes float64 and True becomes 1.0. This automatic conversion is called type promotion (upcasting).
Warning: A string array is useless for maths —
arr.sum()will fail. If your numbers turn into strings, check for a stray text value in your data.
Vectorized operators mean different things
python
import numpy as np
print([1, 2] + [3, 4], np.array([1, 2]) + np.array([3, 4]))
print(np.array([1, 2, 3]) == [1, 2, 3])text
[1, 2, 3, 4] [4 6]
[ True True True]For lists, + joins (concatenates). For arrays, + adds element by element. Comparison == on an array is also element-wise and returns an array of booleans, not a single True/False.
Fixed size — no append
python
import numpy as np
x = np.array([1, 2, 3])
try:
x.append(4)
except AttributeError as e:
print("AttributeError:", e)
y = np.append(x, 4)
print(x, y)text
AttributeError: 'numpy.ndarray' object has no attribute 'append'
[1 2 3] [1 2 3 4]How it works: np.append does not change x; it builds a brand-new, larger array y and copies everything into it. Doing this inside a loop is slow — collect values in a list first (or pre-allocate with np.zeros) and convert once.
Memory comparison
python
import sys
import numpy as np
runs_list = list(range(1000))
runs_arr = np.arange(1000)
list_bytes = sys.getsizeof(runs_list) + sum(sys.getsizeof(x) for x in runs_list)
print("Python list :", list_bytes, "bytes (approx.)")
print("NumPy array :", runs_arr.nbytes, "bytes of data")text
Python list : 36056 bytes (approx.)
NumPy array : 8000 bytes of dataEach small Python int object takes about 28 bytes, plus 8 bytes for the pointer in the list. The NumPy array needs just 8 bytes per int64. (The list number is a rough estimate — CPython reuses objects for small integers — but the point stands: arrays are several times more compact.)
Python list vs NumPy ndarray — full comparison
| Feature | Python list | NumPy ndarray |
|---|---|---|
| Element types | Mixed types allowed | One dtype for all elements (homogeneous) |
| What is stored | Pointers to Python objects | Raw values |
| Memory layout | Scattered objects | Contiguous block |
| Memory use | Higher (~36 bytes per small int) | Lower (8 bytes per int64; even 1 byte for int8/bool) |
| Size | Dynamic: .append(), .extend(), .pop() | Fixed at creation; np.append returns a new array |
+ operator | Concatenates lists | Element-wise addition |
* with an int | Repeats the list | Element-wise multiplication |
| Maths | Needs loops / comprehensions | Vectorized, plus many built-in functions |
| Multi-dimensional | List of lists, a[0][1] | True N-D arrays, a[0, 1], axes |
| Speed for numbers | Slow | Much faster (often 10–100×) |
| Best for | General collections, mixed records, frequent appends | Large numeric data, maths, science, ML |
Common mistakes
- ❌ Expecting
arr.append(5)to work. ✅ Usenp.append(arr, 5)(returns a new array) — or better, build a list and convert once. - ❌ Mixing a string into numeric data:
np.array([72, "45"])gives a string array. ✅ Clean the data first, or convert with.astype(int). - ❌ Assuming
list1 + list2andarr1 + arr2behave the same. ✅ Lists concatenate; arrays add element-wise. - ❌ Using
if arr == [1, 2, 3]:— this raises a "truth value is ambiguous" error. ✅ Usenp.array_equal(arr, [1, 2, 3])or(arr == ...).all().
Interview tip
Q: When would you still use a Python list instead of a NumPy array?
A: When the data is mixed-type (e.g. a player's name, team and runs together), when you need to grow the collection frequently with append, or when the collection is small and no heavy maths is involved. For large, numeric, same-type data that you compute on, use an ndarray.
Key points to remember
- A list stores pointers to separate Python objects; an ndarray stores raw values in one contiguous memory block.
- ndarrays are homogeneous: all elements share one dtype. Mixed input is upcast (int → float → string).
- For arrays, + and * are element-wise maths; for lists, + concatenates and * repeats.
- An ndarray has a fixed size — there is no .append(); np.append returns a new array.
- Arrays use far less memory: 8 bytes per int64 versus roughly 36 bytes per small int in a list.
- Comparisons on arrays (arr == x) return a boolean array, not a single True/False.
- Use lists for mixed or growing collections; use arrays for large numeric data and maths.
Try it yourself
Type promotion: what dtype does NumPy pick?
Operators: list vs array
Memory: list vs array
Practice: fix the string trap
Revision Flashcards
Tap a card to flip it and see the answer.
