Python Tutorial
Introduction to NumPy
NumPy (Numerical Python) is the foundation of data science in Python. pandas, matplotlib, scikit-learn, SciPy, PyTorch and TensorFlow are all built on — or interoperate with — its central object, the ndarray: a fast, fixed-type, n-dimensional array of numbers.
Why not just use lists? A Python list stores pointers to separate Python objects, so maths on a million numbers means a million slow Python-level operations. A NumPy array stores raw numbers in one contiguous block of memory and runs operations in optimised C code, typically 10–100× faster and using far less memory. This lesson covers installing NumPy, creating arrays, their key attributes, data types, and the idea of vectorised operations.
Installing and Importing
Install with pip install numpy (it is also included in Anaconda). By universal convention it is imported as import numpy as np. Everything in this module can be run in a Jupyter notebook, VS Code, or as a normal script.
Creating Arrays
np.array([1, 2, 3])— from a list (nested lists give 2-D arrays).np.zeros((2, 3)),np.ones(5),np.full((2, 2), 7),np.eye(3)— filled arrays and the identity matrix.np.arange(start, stop, step)— likerangebut returns an array;np.linspace(start, stop, num)— evenly spaced values including the end point.np.random.default_rng(seed)— the modern random generator:.integers(),.random(),.normal(),.choice().
Attributes and Data Types
Every array has a shape (size of each dimension, e.g. (3, 4) = 3 rows, 4 columns), ndim (number of dimensions), size (total elements), dtype (element type) and itemsize/nbytes (memory). All elements share one dtype: int64, float64, bool, complex128, fixed-width strings and more. Mixing ints and floats upcasts to float; convert explicitly with astype(). Choosing smaller types such as float32 or int8 can save a lot of memory on big data.
Vectorisation
Arithmetic on arrays applies element by element without a Python loop: prices * 1.18 adds 18% tax to every price, a + b adds two arrays position by position, and functions like np.sqrt, np.exp and np.round ("universal functions" or ufuncs) work on whole arrays. Writing code this way — vectorised — is the single most important NumPy habit.
Examples
Creating arrays and inspecting their attributes
import numpy as np
marks = np.array([78, 92, 65, 88])
matrix = np.array([[1, 2, 3], [4, 5, 6]])
print(marks, marks.dtype, marks.shape, marks.ndim)
print(matrix)
print("shape:", matrix.shape, "size:", matrix.size, "bytes:", matrix.nbytes)
print(np.zeros((2, 3)))
print(np.ones(4, dtype=int), np.full(3, 7.5))
print(np.eye(3, dtype=int))
print(np.arange(0, 20, 5), np.linspace(0, 1, 5))
rng = np.random.default_rng(seed=42) # same seed -> same "random" numbers
print(rng.integers(1, 7, size=5)) # five dice rolls
print(rng.normal(loc=170, scale=10, size=3).round(1))
[78 92 65 88] int64 (4,) 1
[[1 2 3]
[4 5 6]]
shape: (2, 3) size: 6 bytes: 48
[[0. 0. 0.]
[0. 0. 0.]]
[1 1 1 1] [7.5 7.5 7.5]
[[1 0 0]
[0 1 0]
[0 0 1]]
[ 0 5 10 15] [0. 0.25 0.5 0.75 1. ]
[1 5 4 3 3]
[179.4 150.5 157. ]
Data types, upcasting and astype()
import numpy as np
print(np.array([1, 2, 3]).dtype, np.array([1, 2.5]).dtype, np.array([True, False]).dtype)
print(np.array([1, 2, 3.7])) # ints upcast to float
prices = np.array(["199.5", "45", "1200"])
as_numbers = prices.astype(float)
print(prices.dtype, "->", as_numbers.dtype, as_numbers.sum())
big = np.arange(1_000_000, dtype=np.float64)
print(big.nbytes // 1_000_000, "MB as float64,", big.astype(np.float32).nbytes // 1_000_000, "MB as float32")
print(np.array([3.9, -3.9]).astype(int)) # astype(int) truncates toward zero
int64 float64 bool
[1. 2. 3.7]
<U5 -> float64 1444.5
8 MB as float64, 4 MB as float32
[ 3 -3]
Vectorised operations versus Python loops
import time
import numpy as np
prices = np.array([120.0, 250.0, 99.0, 560.0])
quantities = np.array([3, 1, 10, 2])
print(prices * 1.18) # tax on every price
print(prices * quantities) # element-wise multiplication
print((prices * quantities).sum(), np.sqrt([16, 25, 81]))
numbers = list(range(2_000_000))
array = np.arange(2_000_000)
start = time.perf_counter()
squares_list = [n * n for n in numbers]
loop_time = time.perf_counter() - start
start = time.perf_counter()
squares_array = array * array
numpy_time = time.perf_counter() - start
print("same result:", squares_list[-1] == squares_array[-1])
print("NumPy faster:", numpy_time < loop_time)
[141.6 295. 116.82 660.8 ]
[ 360. 250. 990. 1120.]
2720.0 [4. 5. 9.]
same result: True
NumPy faster: True
Common Mistakes
- Looping over array elements in Python instead of using vectorised operations.
- Forgetting that all elements share one dtype, so np.array([1, "a"]) turns everything into strings.
- Confusing np.arange (step size, end excluded) with np.linspace (number of points, end included).
- Using the legacy np.random.seed/np.random.rand instead of the recommended np.random.default_rng().
- Expecting astype(int) to round — it truncates; use np.round first.
Key Points to Remember
- NumPy arrays are fast, compact, single-type, n-dimensional containers of numbers.
- Create arrays with array, zeros, ones, full, eye, arange, linspace and default_rng.
- shape, ndim, size and dtype describe an array; astype converts types.
- Vectorised operations apply to every element at once and are much faster than loops.
- NumPy is the base of pandas, matplotlib and scikit-learn.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local project environment for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.