Appendix C: Environment setup
Every code block in this book that is meant to run standalone runs on a stock CPython install at version 3.10 or newer. Most chapters use only the standard library. The code blocks in Chapters 7 through 13, and in the Appendix D solutions for Chapters 7 and 11, also require NumPy 1.26 or newer; every other chapter is stdlib-only. Chapter 29’s durable-counter block and the pki_migration package behind it are POSIX-only: fcntl locking and directory fsync have no Windows standard-library equivalent, so on Windows run them under WSL. No other third-party package is assumed at any point. If a block in the book fails on a machine that satisfies those constraints, that is a bug in the book. Report it to contact@encryptorium.com, or open an issue on the companion code repository.
Chapter 4 refers to this appendix when it notes that an end-to-end pedagogical simulation of Shor’s quantum period-finding step would normally use a general-purpose quantum framework such as Qiskit or Cirq. The environment contract excludes both. The rest of this appendix explains how to satisfy that contract.
What you need installed
Section titled “What you need installed”Two pieces of software, both standard:
- CPython 3.10 or newer. The pedagogical code annotates optional arguments with PEP 604 union syntax (
bytes | None), which requires 3.10. The requirement is a floor, not a range: any release at 3.10 or newer qualifies, including the 3.14 line, stable since October 2025. The book’s continuous integration runs the code blocks and the test suites on CPython 3.12. On Debian and Ubuntu the package ispython3. On macOS the Homebrew packagepython@3.12matches the interpreter CI runs. On Windows the official installer at python.org ships a recent CPython. - git. The companion code repository is at
https://github.com/Encryptorium/book-of-pqc-code. Any recent git release works.
That is the whole list. There is no Node.js, no Docker, no SageMath, no Rust toolchain.
Clone the repository
Section titled “Clone the repository”git clone https://github.com/Encryptorium/book-of-pqc-code.gitcd book-of-pqc-codeThe repository holds the complete code set for all 41 chapters.
The layout:
chapter-code/chNN/is every Python block that chapter prints and CI runs, one file each, ready to run. The files are generated from the chapter text, so what you run is what the page shows,# ==>output markers included. A block taggedno-verifyis a sketch the gate does not run, and is not written there. See the block-count paragraph below.solutions/chNN-<slug>/is the reference implementation for one chapter, as a standalone Python package.exercises/chNN-<slug>/is the same package with the functions that chapter teaches replaced by stubs. Each stub keeps its signature, keeps its docstring where the reference has one, and adds a contract block describing what it owes its caller.tests/chNN/is the pytest suite, including the NIST vector comparisons where a chapter has them. It runs against either tree.
The prose and figures are not in this repository. They are published at https://book.encryptorium.com. The worked solutions to the numbered exercises are Appendix D. What checks your work in exercises/ is tests/chNN/, the same suite that checks solutions/. The reference tree is there to read afterwards, not something the suite diffs your code against.
Create a fresh virtual environment
Section titled “Create a fresh virtual environment”python3 -m venv .venvsource .venv/bin/activateOn Windows PowerShell the activation line is .venv\Scripts\Activate.ps1 instead.
After activation, pip list on an upstream CPython 3.12 or newer venv shows only pip. CPython 3.10 and 3.11 also install setuptools into a new venv, and some distribution-patched builds include setuptools (or wheel) on any version. Either output is correct: pip builds each solutions/chNN-* package in an isolated PEP 517 environment, so a setuptools already present in the venv is inert during the install. One external dependency is required across the book’s reference packages: NumPy 1.26 or newer, declared by six of them (the packages for Chapters 7 through 12). Two more need it transitively, since solutions/ch27-hybrid imports ML-KEM out of solutions/ch11-mlkem and solutions/ch29-pki imports the Chapter 27 combiner. Every other package runs against the standard library alone.
How the code blocks are verified
Section titled “How the code blocks are verified”Of the 283 Python blocks in the book’s prose, 281 carry a # ==> expected-output marker. A gate in the book’s continuous integration extracts each fenced Python block, runs it in isolation, and compares stdout against those markers. The two blocks without a marker are the two tagged no-verify, which the gate skips: both are sketches rather than snippets meant to run standalone. Every other block runs, and a syntax error or a runtime exception fails the gate whatever the marker says.
That gate reads the chapter Markdown, so it runs where the prose lives rather than in the code repository. The blocks reach you anyway. A second gate writes each verified chapter-body block out to chapter-code/chNN/ and runs the file it wrote, so the copy in your clone is the verified copy rather than a transcription of it. That is 205 of the 281. The 75 blocks on the Appendix D pages and the one on this page run under the first gate but are not exported, because Appendix D’s are worked answers whose fuller form is the solutions/ package. The two no-verify sketches are the two the first gate skips. Running one takes nothing beyond the clone, and NumPy for the lattice chapters:
python3 chapter-code/ch01/01-factor-trial-division.py3233 = 53 x 61Each file names the chapter, the section, and the page it came from in a header comment. The two no-verify sketches are not published there, because they are the two the gate does not run.
Between the two gates, every code block printed in the book apart from those two sketches has been executed, and every reference package has its own suite. The two are separate guarantees: a chapter’s listing is verified on its own, and it need not be a cut from the package beside it. Chapter 13 is the case to know about, where the printed LLL runs under NumPy and the package’s estimator is stdlib-only and implements no lattice reduction.
Run a chapter’s pytest suite
Section titled “Run a chapter’s pytest suite”Each package under solutions/chNN-<slug>/ ships with a matching pytest suite at tests/chNN/. A chapter earns a package and a suite by having code worth testing, which is not the same as printing code in its body. Chapter 3 prints none, so it has no chapter-code/ch03/. Its Appendix D answers are executable, so it ships a solutions/, an exercises/ and a tests/ch03 like any other chapter. Pytest itself is not part of the standard library and is not declared as a dependency of any chapter’s package, since the chapters’ runtime code never imports it. Install pytest once into the venv before running any test suite:
pip install pytestThen run one chapter from the repository root:
pytest tests/ch04 -xNo package install is needed first. Each tests/chNN/conftest.py puts the chapter package’s src/ directory at the front of sys.path, so the suite imports the package straight out of the clone.
For the flagship implementations, test_vectors.py under the chapter’s test directory checks package output against the official NIST Automated Cryptographic Validation Protocol (ACVP) vectors byte-for-byte. For ML-KEM (Chapter 11), keygen, encapsulation, and decapsulation each compare against ACVP vectors at all three parameter sets. For ML-DSA (Chapter 12), key generation and signing reproduce the ACVP vectors byte for byte at all three parameter sets, and verification accepts each genuine signature and rejects each tampered one.
For SLH-DSA (Chapter 17), four checks run. The twelve parameter sets are pinned against FIPS 205 Table 2, and key generation reproduces the ACVP vectors at all twelve. Verification accepts every valid ACVP case at all twelve and rejects the invalid cases, which the fixture supplies at the two 128s sets. Signature generation is reproduced byte for byte at the six fast sets. The six small-signature sets are too slow to run under CI and skip rather than being dropped silently. A clean run ends with passed or skipped against every assertion. If a vector fails, the book’s implementation diverged from the published specification, and that is a bug.
A pip install -e solutions/chNN-<slug> is optional. It changes where an interactive import resolves from, not what the suite does. Install at most one tree. The exercise package is a copy of the reference package, pyproject.toml included, so both declare the same distribution name, and pip treats the second install as replacing the first rather than keeping both. Chapters 7 through 12 need NumPy in the venv either way. See the “Part II addendum: NumPy for Chapters 7 through 13” section below.
Implementing a chapter yourself
Section titled “Implementing a chapter yourself”The test suite selects which tree it imports from with the PQC_IMPL environment variable. It defaults to solutions, so a fresh clone is green:
pytest tests/ch04Set it to exercises to work against the stubs instead:
PQC_IMPL=exercises pytest tests/ch04On Windows PowerShell that line is an assignment, a run and a reset:
$env:PQC_IMPL = 'exercises'python -m pytest tests/ch04Remove-Item Env:PQC_IMPLEvery stubbed function raises NotImplementedError until you write it, so the first run is red by design. Open exercises/ch04-classical-to-pq/, implement one function, and run the suite again. The failures narrow as you go.
PQC_IMPL switches every package in the clone at once, not only the chapter you are working on, and a few chapters import another chapter’s package. Chapter 27 imports Chapter 11’s ML-KEM, and Chapter 29 imports Chapter 27 and Chapter 15’s XMSS. So a Chapter 29 suite run under exercises fails inside Chapter 15’s xmss_keygen stub, at fixture setup, before it reaches anything you were asked to write. Implement the chapters it depends on first, or run that chapter against solutions until you do.
Every chapter that has a reference package also has a stub package, so the three trees keep step: a solutions/chNN-<slug> package, a matching exercises/chNN-<slug> package, and a tests/chNN directory ship together. PQC_IMPL=exercises resolves for every test directory present in the clone. There is no chapter for which it collects nothing. What a chapter can lack is chapter-code/chNN/, which holds only the listings its own body prints: Chapter 3 prints none and has no such directory, while still shipping all three of the other trees.
PQC_IMPL accepts only solutions or exercises. Any other value stops with an error naming the variable rather than a confusing import failure.
Two things worth knowing. Most stubs carry the reference implementation’s own docstring, so the contract you implement against is the one the book describes rather than a paraphrase. The rest are functions the reference itself wrote without a docstring, mostly one-line address accessors and field-arithmetic helpers, and there the contract block carries the whole contract. And nothing is hidden: solutions/ is in the same clone. Reading it costs you the exercise, which is a trade only you can price.
Part II addendum: NumPy for Chapters 7 through 13
Section titled “Part II addendum: NumPy for Chapters 7 through 13”The lattice construction chapters (Chapter 7 through Chapter 12) represent Module-LWE secrets, public keys, ciphertexts, and signatures as NumPy integer arrays. The pedagogical cost of a pure-Python implementation is that the linear-algebra content disappears inside hand-rolled loops, and the Module-LWE matrix-vector products become the dominant runtime in any full vector check. NumPy keeps both the prose and the run-time compact without adding any abstraction the reader cannot follow.
To run any Part II chapter, install NumPy inside the venv:
pip install "numpy>=1.26"This is the only third-party dependency any chapter in the book introduces. Installing a Part II package with pip does this automatically:
pip install -e solutions/ch11-mlkemAfter the install, pip list shows numpy at a version 1.26 or newer. The Chapter 13 package (lattice cryptanalysis) is stdlib-only: it uses Python’s math module for the closed-form root-Hermite-factor cost estimate and declares no dependencies. The chapter itself still needs NumPy, for the toy LLL block that runs the Kannan embedding.
Quantum simulators are out of scope
Section titled “Quantum simulators are out of scope”Chapter 4 builds toy RSA and toy ECDSA from scratch and then walks the classical post-processing of Shor’s algorithm on a 9-bit modulus in a few lines of gcd arithmetic. What the chapter deliberately does not do is simulate the quantum period-finding subroutine. Simulating a useful Shor circuit classically is itself exponential in the qubit count. An end-to-end pedagogical implementation would normally use a general-purpose quantum framework such as Qiskit or Cirq, and building equivalent machinery from scratch would be a substantial detour. Pulling either into the reader’s venv would move the chapter’s focus from classical post-processing to simulator setup, which is a different book.
If you want to see the quantum part end-to-end, the original Shor paper (Shor, 1994) and the Nielsen and Chuang textbook (Nielsen & Chuang, 2010) walk the circuit in full. The book references both at the point where the classical slice ends.
Smoke test
Section titled “Smoke test”Once Python is installed and the venv is active, this block confirms the environment:
import hashlib
sha = hashlib.sha256(b"book-of-pqc setup ok").hexdigest()[:16]print(sha)# ==> e0ae8dc9e81b9052The printed prefix is e0ae8dc9e81b9052 wherever the block is run as written: SHA-256 of the same bytes does not depend on the interpreter or on the venv. A different prefix means the block itself was altered (the string, the slice, or quotes changed by an editor). The block does not check which interpreter is active. For that, run python3 --version and which python3 (or where python on Windows) and confirm the venv’s path.
Common setup issues
Section titled “Common setup issues”A few things that trip readers up most often.
”No module named ‘numpy’” in a Part II chapter
Section titled “”No module named ‘numpy’” in a Part II chapter”The venv is active but numpy was not installed. Run pip install "numpy>=1.26" inside the venv and retry.
Pytest reports “no tests ran”
Section titled “Pytest reports “no tests ran””Pytest was called from somewhere other than the repository root. The chapter test directories resolve relative to it, so cd into the clone and run pytest tests/chNN from there.
python3 --version reports 3.8 or 3.9
Section titled “python3 --version reports 3.8 or 3.9”Some reference packages annotate an optional argument with a PEP 604 union such as bytes | None without importing annotations from __future__. On both versions the annotation is evaluated when the function is defined, so importing one of those modules raises TypeError: unsupported operand type(s) for | rather than a SyntaxError. Install CPython 3.10 or newer. If a system package manager pins an older version, the official CPython installer at python.org bypasses that pin cleanly.
The venv activates but which python still points at the system CPython
Section titled “The venv activates but which python still points at the system CPython”Check first that the activation script was sourced rather than run. Sourcing is source .venv/bin/activate. Running the script instead edits the PATH of a subshell that then exits, so the shell you are in never sees it. If it was sourced, check PATH and any python alias, since activation prepends the environment’s own executable directory and the upstream script resets the command hash itself.
A chapter’s code block runs in the book but fails when pasted into a REPL
Section titled “A chapter’s code block runs in the book but fails when pasted into a REPL”The verification gate runs each block in a fresh interpreter. Variables from earlier blocks do not leak into later ones. If a chapter splits a computation across two blocks, the second block restates the variables it needs, and the REPL needs the same.
Exit condition
Section titled “Exit condition”A reader following the steps above has a venv with CPython 3.10 or newer, a fresh clone of the code repository, and (for the lattice chapters) NumPy 1.26 or newer. Running pytest tests/ from the repository root collects the full 41-chapter suite and should report every test as passing or skipped and none as failing, because the suite defaults to the reference implementation. On Windows outside WSL there is one exception: the whole tests/ch29 suite fails at collection, because the package it tests imports the POSIX-only fcntl module (the Chapter 29 carve-out above). Some cases still skip by design, most visibly the SLH-DSA signature-generation vectors at the six small-signature parameter sets, which are too slow to run under CI. Pytest prints an s for each skip without saying why. Add -rs to the command to print the reason for every one. From that point, every chapter’s code is reproducible, and PQC_IMPL=exercises turns a chapter that ships a stub package into an exercise.