PythonIntro to NumPy

Intro to NumPy

NumPy ("Numerical Python") is the foundation of numerical computing in Python. Plain Python lists are flexible — they can hold anything — but that flexibility has a cost: every element is a full Python object with its own type info and memory overhead, and operations on lists run through the Python interpreter one element at a time. NumPy replaces this with the ndarray, a contiguous block of memory holding elements of a single, fixed type (like 64-bit floats), and it performs operations on entire arrays using fast, compiled C loops instead of Python-level loops.

Installing NumPy

Bash
pip install numpy
Creating arrays

There are several common ways to build an array, depending on whether you already have the data or need to generate it:

Python
import numpy as np

# From an existing list
a = np.array([1, 2, 3, 4])
print(a)          # [1 2 3 4]
print(a.dtype)    # int64 (or int32 on some platforms)

# A 2D array from a list of lists
grid = np.array([[1, 2, 3], [4, 5, 6]])
print(grid.shape)  # (2, 3)  -> 2 rows, 3 columns

# Filled with zeros
zeros = np.zeros((3, 3))

# A range of values, like Python's range()
r = np.arange(0, 10, 2)     # [0 2 4 6 8]

# N evenly spaced values between two endpoints
lin = np.linspace(0, 1, 5)  # [0.   0.25 0.5  0.75 1.  ]
Vectorized operations vs Python loops

The single biggest reason to reach for NumPy is vectorization: writing a + b instead of looping over each pair of elements. Under the hood, NumPy dispatches the whole operation to a tight loop written in C, operating directly on contiguous memory — no per-element Python object creation, no interpreter bytecode dispatch for every addition, and on top of that the CPU can often process multiple elements per instruction (SIMD). The result is typically one to two orders of magnitude faster than the equivalent pure-Python loop.

Python
import numpy as np
import time

n = 1_000_000
py_list = list(range(n))
np_array = np.arange(n)

# Pure Python loop
start = time.time()
py_result = [x * 2 for x in py_list]
print("Python loop:", time.time() - start)

# Vectorized NumPy
start = time.time()
np_result = np_array * 2
print("NumPy vectorized:", time.time() - start)
Python loop: 0.061s
NumPy vectorized: 0.002s
Rule of thumb
If you ever find yourself writing a `for` loop over a NumPy array to do arithmetic, there is almost always a vectorized way to express the same operation directly on the whole array.
Shape, indexing, and slicing

Every array has a .shape (a tuple describing its dimensions) and supports slicing similar to Python lists, extended to multiple dimensions using a comma inside the brackets:

Python
grid = np.array([
    [ 1,  2,  3,  4],
    [ 5,  6,  7,  8],
    [ 9, 10, 11, 12],
])

print(grid.shape)     # (3, 4)
print(grid[0, 0])     # 1        -> row 0, column 0
print(grid[1, :])     # [5 6 7 8] -> entire row 1
print(grid[:, 2])     # [ 3  7 11] -> entire column 2
print(grid[0:2, 1:3])
# [[2 3]
#  [6 7]]  -> a 2x2 sub-block
Broadcasting

Broadcasting lets NumPy apply an operation between arrays of different (but compatible) shapes without you writing an explicit loop or manually duplicating data. A smaller array is conceptually "stretched" to match the larger one's shape:

Python
prices = np.array([10, 20, 30])   # shape (3,)
discount = 0.9                     # a single scalar

# The scalar is broadcast across every element
sale_prices = prices * discount
print(sale_prices)  # [ 9. 18. 27.]

matrix = np.array([[1, 2, 3], [4, 5, 6]])  # shape (2, 3)
row = np.array([10, 20, 30])               # shape (3,)

# 'row' is broadcast across every row of 'matrix'
print(matrix + row)
# [[11 22 33]
#  [14 25 36]]
Note
Broadcasting rules compare shapes from the right: dimensions are compatible if they're equal, or one of them is 1. If shapes are incompatible, NumPy raises a `ValueError` rather than guessing what you meant.
Why this matters beyond NumPy itself

NumPy arrays aren't just a convenience for math scripts — they are the shared data structure underneath most of the Python data ecosystem. pandas DataFrames store their columns as NumPy arrays, scikit-learn expects NumPy arrays as input to its models, and TensorFlow and PyTorch tensors are directly convertible to and from NumPy arrays. Learning NumPy's array model is what makes all of those libraries click.