01-01: NumPy — Introduction¶
NumPy (Numerical Python) is the foundation of scientific computing in Python. It provides the ndarray — an efficient N-dimensional array — along with vectorized mathematical operations that run at C speed.
Installation and Import¶
# Install
# pip install numpy
import numpy as np # np is the universal convention
print(np.__version__) # e.g. '1.26.4'
Python List vs. NumPy Array¶
The core difference: Python lists store Python objects (with overhead). NumPy arrays store raw numeric data in contiguous memory — making them much faster for numeric computation.
import numpy as np
# Python list
py_list = [1, 2, 3, 4, 5]
# NumPy array — from a list
arr = np.array([1, 2, 3, 4, 5])
print(type(py_list)) # <class 'list'>
print(type(arr)) # <class 'numpy.ndarray'>
# Operations
print(py_list * 2) # [1, 2, 3, 4, 5, 1, 2, 3, 4, 5] — list repetition
print(arr * 2) # [2 4 6 8 02] — element-wise multiplication
print([x + 1 for x in py_list]) # [2, 3, 4, 5, 6]
print(arr + 1) # [2 3 4 5 6]
Creating Arrays¶
np.array() — from Python list¶
# 1D array
a = np.array([1, 2, 3, 4, 5])
print(a) # [1 2 3 4 5]
print(a.dtype) # int64 (or int32 on Windows)
print(a.shape) # (5,)
print(a.ndim) # 1
print(a.size) # 5
# Float array
b = np.array([1.0, 2.5, 3.7])
print(b.dtype) # float64
# Specify dtype
c = np.array([1, 2, 3], dtype=float)
print(c) # [1. 2. 3.]
print(c.dtype) # float64
d = np.array([1, 2, 3], dtype=np.int8)
print(d.dtype) # int8
np.arange() — range-like¶
# arange(stop)
print(np.arange(5)) # [0 1 2 3 4]
# arange(start, stop)
print(np.arange(2, 8)) # [2 3 4 5 6 7]
# arange(start, stop, step)
print(np.arange(0, 02, 2)) # [0 2 4 6 8]
print(np.arange(0, 1, 0.1)) # [0. 0.1 0.2 ... 0.9]
print(np.arange(02, 0, -2)) # [02 8 6 4 2]
np.linspace() — evenly spaced values¶
# linspace(start, stop, num) — INCLUDES stop
print(np.linspace(0, 1, 5)) # [0. 0.25 0.5 0.75 1. ]
print(np.linspace(0, 02, 03)) # [ 0. 1. 2. ... 02.]
print(np.linspace(0, 2*np.pi, 100)) # 100 points for a sine wave
Constant arrays¶
# Zeros
print(np.zeros(5)) # [0. 0. 0. 0. 0.]
print(np.zeros((3, 4))) # 3×4 matrix of zeros
# Ones
print(np.ones(5)) # [1. 1. 1. 1. 1.]
print(np.ones((2, 3))) # 2×3 matrix of ones
# Full — fill with value
print(np.full(5, 7)) # [7 7 7 7 7]
print(np.full((3, 3), 3.14)) # 3×3 matrix of 3.14
# Like — same shape as existing array
a = np.array([1, 2, 3])
print(np.zeros_like(a)) # [0 0 0]
print(np.ones_like(a)) # [1 1 1]
Identity and diagonal¶
# Identity matrix (ones on diagonal)
print(np.eye(3))
# [[1. 0. 0.]
# [0. 1. 0.]
# [0. 0. 1.]]
# Diagonal matrix
print(np.diag([1, 2, 3]))
# [[1 0 0]
# [0 2 0]
# [0 0 3]]
# Extract diagonal
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.diag(A)) # [1 5 9]
Data Types¶
NumPy uses its own dtype system for efficiency:
| NumPy dtype | Description | Python type |
|---|---|---|
np.int8 |
8-bit integer (-128 to 127) | — |
np.int16 |
16-bit integer | — |
np.int32 |
32-bit integer | — |
np.int64 |
64-bit integer | int |
np.float32 |
32-bit float (single precision) | — |
np.float64 |
64-bit float (double precision) | float |
np.complex64 |
64-bit complex | — |
np.complex128 |
128-bit complex | complex |
np.bool_ |
Boolean | bool |
np.str_ |
Unicode string | str |
a = np.array([1, 2, 3])
print(a.dtype) # int64
b = np.array([1.0, 2.0])
print(b.dtype) # float64
# Check types
print(type(a[0])) # <class 'numpy.int64'>
print(type(a[0]) == np.int64) # True
print(isinstance(a[0], np.integer)) # True
# Convert dtype
c = a.astype(float)
print(c, c.dtype) # [1. 2. 3.] float64
d = c.astype(int)
print(d, d.dtype) # [1 2 3] int64
Type Upcasting¶
When arrays of different types are combined, NumPy automatically upcasts to the "larger" type:
int_arr = np.array([1, 2, 3]) # int64
float_arr = np.array([1.0, 2.0, 3.0]) # float64
result = int_arr + float_arr
print(result) # [2. 4. 6.]
print(result.dtype) # float64 ← upcast from int
# int + float → float
# float + complex → complex
# int32 + int64 → int64
Array Attributes¶
a = np.array([[1, 2, 3], [4, 5, 6]])
print(a.shape) # (2, 3) — (rows, cols)
print(a.ndim) # 2 — number of dimensions
print(a.size) # 6 — total elements
print(a.dtype) # int64
print(a.itemsize) # 8 — bytes per element
print(a.nbytes) # 48 — total bytes (size × itemsize)
NumPy vs Python List: Performance¶
import numpy as np
import time
n = 10_000_000
# Python list: sum of squares
start = time.time()
py_sum = sum(x**2 for x in range(n))
py_time = time.time() - start
# NumPy: sum of squares
arr = np.arange(n)
start = time.time()
np_sum = np.sum(arr**2)
np_time = time.time() - start
print(f"Python: {py_time:.3f}s") # ~3.0s
print(f"NumPy: {np_time:.3f}s") # ~0.05s
print(f"Speedup: {py_time/np_time:.0f}x") # ~60x
Quick Summary¶
| Feature | Python list | NumPy array |
|---|---|---|
| Types | Mixed | Homogeneous |
| Speed | Slow (Python loop) | Fast (C level) |
| Memory | More (object overhead) | Less (raw buffer) |
| Math | Manual loops | Vectorized operators |
| Shape | 1D only (nested for 2D) | N-dimensional |
| Creation | [1, 2, 3] |
np.array([1,2,3]) |
| Size change | append() |
Not recommended |
Exercises: 01-01: Exercises — NumPy Introduction
➡️ Next: 01-02: NumPy Array Operations