Appendix C — Python and numpy Setup Guide

Every chapter of this book pairs the mathematics with a short numpy snippet, and the progressive project builds a from-scratch toolkit/ you verify against numpy and scipy. None of that requires you to be a Python expert — but it does require a working Python with the right libraries installed, in a place that will not collide with the rest of your system. This appendix gets you there in about fifteen minutes, then hands you a "hello, linear algebra" snippet and a short list of the gotchas that trip up nearly everyone the first time mathematics meets code.

You do not need to run anything to follow the book — every snippet shows its output so you can read along on paper. But linear algebra is a subject you learn with your hands, and the fastest way to believe a theorem is to watch numpy confirm it. Set this up once; you will use it in all forty chapters.

C.1 Installing Python 3.10 or newer

The book targets Python 3.10+ (the snippets use modern syntax, and the libraries below have dropped support for older versions). Check what you have first — open a terminal (Terminal on macOS/Linux, PowerShell or Command Prompt on Windows) and run:

python --version        # or: python3 --version

If it prints Python 3.10.x or higher, you are set. If the command is missing or the version is older, install a current Python:

  • Windows / macOS: download the official installer from python.org/downloads and run it. On Windows, tick the box "Add Python to PATH" during installation — skipping it is the most common first-day snag, because then python is not found in a fresh terminal.
  • macOS (Homebrew): brew install python.
  • Linux: use your package manager, e.g. sudo apt install python3 python3-pip python3-venv on Debian/Ubuntu.

Common Pitfall — On many systems python still points at a legacy Python 2, while Python 3 is python3. If python --version surprises you, try python3. The same split applies to the package installer: pip versus pip3. Inside a virtual environment (next section) this ambiguity disappears — python and pip mean the environment's versions — which is one more reason to always work in one.

C.2 Creating a virtual environment (do this once per project)

A virtual environment ("venv") is a private, self-contained Python that belongs to this project alone. Installing the book's libraries into a venv keeps them from clashing with other projects or with your operating system's Python. This is standard practice, not a nicety; get in the habit now.

From the folder where you keep your linear-algebra work (for instance, the folder containing the book's toolkit/), create and activate a venv:

# Create a venv named ".venv" in the current folder (once)
python -m venv .venv

# Activate it (every time you open a new terminal for this project)
source .venv/bin/activate          # macOS / Linux
.venv\Scripts\activate             # Windows PowerShell / cmd

Once activated, your prompt shows (.venv) and python/pip refer to the environment. To leave it, type deactivate. Re-activate it whenever you return — the packages stay installed; only the activation is per-session.

C.3 Installing the libraries

With the venv active, install the four libraries the book uses. Upgrade pip first so you get current wheels:

python -m pip install --upgrade pip
python -m pip install numpy scipy matplotlib pillow

Here is what each one is for in this book:

Library Role Where it appears
numpy arrays and core linear algebra (np.linalg) every chapter
scipy extra factorizations (scipy.linalg: LU, expm, null_space, Cholesky) Ch. 10, 20, 28, 37, 38
matplotlib all figures, including the 2D transformation visualizer every geometric chapter
pillow loading/saving images as arrays the SVD image-compression chapters (30–31)

Confirm everything imports and report its version:

# check_install.py — confirm the toolchain is ready
import numpy as np, scipy, matplotlib, PIL
print("numpy", np.__version__, "| scipy", scipy.__version__)
print("matplotlib", matplotlib.__version__, "| pillow", PIL.__version__)
# Example output:
# numpy 2.3.5 | scipy 1.17.0
# matplotlib 3.x | pillow 11.x

If those three lines print without error, you are ready for Chapter 1. (Exact version numbers will differ; any recent release works — the book avoids bleeding-edge API.)

C.4 Using the book's from-scratch toolkit/

The progressive project lives in a package called toolkit/. Each chapter's "Build Your Toolkit" callout asks you to implement one operation from scratch in pure Python (no numpy inside the implementation — that is the point), and the matching test in toolkit/tests/ checks your work against numpy/scipy. The layout looks like this:

toolkit/
├── __init__.py
├── vectors.py            # Ch. 2:  add, scale, dot, norm, angle
├── matrices.py           # Ch. 7/8: matmul (as composition), transpose
├── linear_systems.py     # Ch. 4:  row_reduce, gaussian_elimination, back_substitute
├── ...                   # inverse, lu, determinant, projection, gram_schmidt, eigen, svd, pca
└── tests/                # numpy/scipy live here ONLY, as the trusted reference

Run a chapter's tests from the folder that contains toolkit/, using the -m/module form so Python finds the package on its path:

# from the directory that contains the toolkit/ folder
python -m pytest toolkit/tests/test_linear_systems.py     # one chapter's tests
python -m pytest toolkit/tests/                           # the whole suite

A test file can also be run directly (python toolkit/tests/test_linear_systems.py) for plain pass/fail output. The division of labor is the book's third recurring theme in miniature: your pure-Python code builds understanding; numpy verifies it at scale. Implement, then verify — never the other way around.

Build Your Toolkit — Before Chapter 2, create the folder, drop an empty __init__.py inside it, and run python -m pytest toolkit/tests/ from the parent directory. Seeing "no tests ran" (rather than an import error) confirms Python can find the package — your scaffold is ready for the first real function.

C.5 Hello, linear algebra

Here is a complete first program. It builds a $2\times2$ matrix, applies it to a vector, solves a system, and confirms the solution — touching the operations of Chapters 2 through 9 in nine lines.

# hello_linalg.py — the first thing to run
import numpy as np

A = np.array([[2.0, 1.0],
              [1.0, 3.0]])          # a 2x2 matrix (italic capital A in the book)
x = np.array([1.0, 2.0])             # a vector (bold lowercase x in the book)

print("A @ x =", A @ x)              # matrix-vector product -> [4. 7.]
b = np.array([4.0, 7.0])
solution = np.linalg.solve(A, b)     # solve A x = b
print("solve   =", solution)         # -> [1. 2.], recovering x
print("match?  =", np.allclose(solution, x))   # -> True

Save it, activate your venv, and run python hello_linalg.py. Three lines of output, the last of which is True, mean your installation is sound and you have just performed the central act of the whole subject: applied a matrix to a vector and inverted the operation.

C.6 The gotchas everyone hits (read this once, save yourself hours)

These are the recurring traps where the mathematics of the book and the mechanics of numpy disagree. The book flags each one the first time it bites in a chapter; here they are collected so you meet them on purpose.

  • Math indexes from 1; numpy indexes from 0. The mathematician's first component $v_1$ is v[0] in code, and the entry $a_{11}$ (top-left) is A[0, 0]. The rule, stated once: $a_{ij}$ in math is A[i-1, j-1] in numpy. This single off-by-one causes more confusion than any other detail when you check a hand computation against code.

  • Never compare floats with ==. Floating-point arithmetic is approximate: 0.1 + 0.2 == 0.3 evaluates to False in Python. Comparing computed matrices, eigenvalues, or norms with == will betray you. Use a tolerance instead: np.allclose(x, y) for arrays, np.isclose(a, b) for scalars. Throughout the book, "the hand result matches numpy" always means np.allclose, never ==.

  • @ is matrix multiplication; * is elementwise. This is the error that produces wrong answers silently, because both run without complaint. A @ B is the matrix product of Chapter 8 (composition of transformations). A * B multiplies entry by entry (the Hadamard product), which is almost never what a linear-algebra formula means. Likewise A @ x is the matrix–vector product; A * x broadcasts and is usually a bug. When you transcribe a formula like $A^{\mathsf{T}}A$, write A.T @ A.

  • np.linalg.eig versus np.linalg.eigh. Use eigh for symmetric/Hermitian matrices and eig for everything else. eigh exploits symmetry to return real eigenvalues sorted in ascending order with guaranteed orthonormal eigenvectors — matching the Spectral Theorem (Ch. 27). eig is general and returns eigenvalues in no guaranteed order, possibly complex (a rotation matrix yields a complex pair), with eigenvectors that are merely scaled to unit length. Calling eig on a symmetric matrix still works but throws away the symmetry guarantees and can introduce tiny imaginary parts from rounding. Match the tool to the matrix.

  • SVD and eigenvectors have sign and order conventions. U, s, Vt = np.linalg.svd(A) returns the singular values s in descending order (good — matches Eckart–Young, Ch. 31), and Vt is already $V^{\mathsf{T}}$, not $V$ (so reconstruct with U @ np.diag(s) @ Vt, and the right singular vectors are the rows of Vt). But the sign of each singular vector is not unique: $(\mathbf{u}_i,\mathbf{v}_i)$ and $(-\mathbf{u}_i,-\mathbf{v}_i)$ are equally valid, so different machines or library versions may flip a column. Eigenvectors from eig/eigh carry the same sign ambiguity (and eig's ordering is not even fixed). The cure is to compare quantities that are unique — the singular values, the subspace a vector spans, or the reconstructed matrix $A=U\Sigma V^{\mathsf{T}}$ — rather than the raw signed vectors. Appendix D returns to this in the cheatsheet.

  • Trig functions take radians, not degrees. np.cos, np.sin expect radians, so a $90^\circ$ rotation uses np.cos(np.pi/2), and np.deg2rad(90) converts if you think in degrees. Feeding degrees to np.sin is a quiet source of nonsense rotations (Ch. 21).

The Key Insight — Almost every numpy surprise in this book traces to one of three roots: an index that starts at 0 instead of 1, a float compared exactly instead of with a tolerance, or * written where @ was meant. Internalize those three and the code will stop fighting you — leaving you free to watch it confirm the mathematics, which is the whole reason it is here.