NumPy · lesson 2 of 13
Introduction to NumPy
Why NumPy exists: measure a Python list against an ndarray for memory and speed, install NumPy, and inspect your first array's dtype and shape.
By SkillByExample · Updated
What Is NumPy?
NumPy is the array library the rest of scientific Python is built on. Its ndarray holds many
values of one type in a single block of memory, and its functions operate on the whole block at
once — no Python loop required.
Memory: A List vs an Array
The same million integers, stored both ways:
import sys
import numpy as np
n = 1_000_000
py_list = list(range(n))
arr = np.arange(n)
list_bytes = sys.getsizeof(py_list) + sum(sys.getsizeof(x) for x in py_list)
print(f"list : {list_bytes / 1e6:.1f} MB")
print(f"array: {arr.nbytes / 1e6:.1f} MB")
print(arr.dtype, arr.itemsize, "bytes per element")
$ python memory.py
list : 36.0 MB
array: 8.0 MB
int64 8 bytes per element
The list pays for a pointer per slot plus a full Python int object per value (28 bytes
each), so it lands at several times the array’s size. The array stores each value as a raw
8-byte int64 and nothing else — nbytes is exactly n * itemsize.
Speed: A Loop vs a Vectorized Operation
Doubling every element, once with a list comprehension and once with NumPy:
import timeit
import numpy as np
n = 10_000_000
py_list = list(range(n))
arr = np.arange(n)
t_list = min(timeit.repeat(lambda: [x * 2 for x in py_list], number=1, repeat=5))
t_np = min(timeit.repeat(lambda: arr * 2, number=1, repeat=5))
print(f"list comprehension: {t_list:.3f} s")
print(f"numpy arr * 2 : {t_np:.3f} s")
print(f"speedup : {t_list / t_np:.0f}x")
$ python speed.py
list comprehension: 0.458 s
numpy arr * 2 : 0.055 s
speedup : 8x
Each time is the best of five runs (timeit.repeat + min), which filters out background
noise. Even so, the ratio is not a fixed number: rerunning this script on the same machine gave
anywhere from about 8x to 26x, mostly because the NumPy side is so short that allocating the
80 MB result dominates it. The list side never came close. The comprehension executes Python bytecode ten million times; arr * 2 is a single call into a
compiled loop that walks the array’s memory directly.
Installation
Install with pip install numpy (or conda install numpy), then confirm which version you have:
import numpy as np
print(np.__version__)
$ python version.py
2.5.3
Every example in this track was run against the version shown above. NumPy 2.x changed a few printed representations (scalars show their type), so if you are on 1.x some output will look slightly different.
Your First Array
import numpy as np
temps_c = np.array([22.1, 23.5, 19.8, 25.0, 21.3])
print(temps_c)
print(temps_c.dtype, temps_c.shape, temps_c.ndim, temps_c.size)
temps_f = temps_c * 9 / 5 + 32
print(temps_f)
print("mean:", temps_c.mean())
print("repr:", repr(temps_c.mean()))
$ python first_array.py
[22.1 23.5 19.8 25. 21.3]
float64 (5,) 1 5
[71.78 74.3 67.64 77. 70.34]
mean: 22.34
repr: np.float64(22.34)
NumPy inferred float64 from the decimal literals, and shape (5,) is a one-element tuple
because the array has one axis. temps_c * 9 / 5 + 32 converted all five readings with no
loop. The last two lines show NumPy 2’s scalar printing: print gives the bare number, while
repr (what you see inside a list or at the REPL) names the type as np.float64(...).
A 2-D Array
import numpy as np
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(matrix.shape)
print(matrix.ndim)
print(matrix.dtype)
print(matrix.nbytes)
$ python matrix_props.py
(3, 3)
2
int64
72
A nested list of equal-length rows becomes a 2-D array with shape (rows, columns). Integer
literals default to int64 on Linux and macOS (and on Windows since NumPy 2.0), so nine
elements take 72 bytes.
Practice
- Change the memory example to use
np.arange(n, dtype=np.int32). Predict the new array size before running it. - In the speed example, replace
x * 2withx ** 2in both versions. Does the speedup grow or shrink? - Build a 2 × 4 array of floats and print its
shape,size, andnbytes.
Solution 1
import numpy as np
n = 1_000_000
arr = np.arange(n, dtype=np.int32)
print(f"array: {arr.nbytes / 1e6:.1f} MB")
print(arr.dtype, arr.itemsize, "bytes per element")
$ python practice_int32.py
array: 4.0 MB
int32 4 bytes per element
Halving the element width halves the array: int32 uses 4 bytes per value instead of 8.
Solution 2
import timeit
import numpy as np
n = 10_000_000
py_list = list(range(n))
arr = np.arange(n)
t_list = min(timeit.repeat(lambda: [x ** 2 for x in py_list], number=1, repeat=5))
t_np = min(timeit.repeat(lambda: arr ** 2, number=1, repeat=5))
print(f"list: {t_list:.3f} s numpy: {t_np:.3f} s speedup: {t_list / t_np:.0f}x")
$ python practice_square.py
list: 0.704 s numpy: 0.035 s speedup: 20x
The list side got slower (Python’s ** costs more than *), while NumPy’s time barely moved.
Reruns on the same machine gave 15x to 20x, inside the range the doubling example showed, so
don’t read much into any single ratio. What doesn’t change is that the list side does one
interpreted step per element and NumPy does none.
Solution 3
import numpy as np
a = np.array([[1.0, 2.0, 3.0, 4.0],
[5.0, 6.0, 7.0, 8.0]])
print(a.shape, a.size, a.nbytes)
$ python practice_shape.py
(2, 4) 8 64
Eight float64 values at 8 bytes each is 64 bytes.