Case Study 1: The Environment That Fought Back
"It works on my machine" is not a defense. It is a description of the bug.
Executive Summary
A working environment is the least interesting and most abandonment-causing part of learning quantum
programming. More people give up in week one over a ModuleNotFoundError than ever give up over
entanglement.
This case study walks through a realistic broken setup — the kind assembled from four small, individually reasonable decisions — and repairs it from a dead stop to a clean, reproducible, verified install. The specific bugs matter less than the method: how to find out what your environment actually is, rather than what you believe it to be. That method transfers to every Python project you will ever debug, and it is unusually valuable here because the quantum stack has deep dependency trees and a history of breaking changes.
Skills applied: virtual environments and interpreter identity (§2.1); the three-package split and what each owns (§2.2); credential storage (§2.3); the error taxonomy (§2.8).
Background
You are helping a colleague get started. They are a competent Python developer with a decade of experience, they followed a tutorial, and nothing works. Here is what they did, in order, all of it reasonable:
- Opened a terminal and ran
pip install qiskit. - Hit an error about a missing simulator, searched, and ran
pip install qiskit-aer. - Found a tutorial with
from qiskit import execute, which failed, and — reading a Stack Overflow answer from 2022 — ranpip install qiskit==0.45to "get the version the tutorial uses." - Put their API token in a file called
quantum_config.pyand imported it.
Their report: "Qiskit is installed, I can see it in pip list, but Python says it isn't there. And when it does work, half the tutorials still fail."
Four decisions, four problems, and they interact. This is realistic. Broken environments rarely have one cause; they have a stack of them, and you peel them off one at a time.
Diagnosis
Step 1: Find out what interpreter is actually running
Never begin by reinstalling. Begin by finding out what is true.
import sys
print(sys.executable)
print(sys.version)
for p in sys.path:
print(" ", p)
/usr/bin/python3
3.9.16 (main, Dec 7 2022, 01:11:51)
/usr/lib/python39.zip
/usr/lib/python3.9
/usr/lib/python3.9/lib-dynload
/home/colleague/.local/lib/python3.9/site-packages
Two findings already, and both matter.
Python 3.9. Modern Qiskit requires 3.10 or newer. Some things will install and some will not, producing partial, confusing failures rather than a clean refusal.
No virtual environment. Packages are going to ~/.local/lib/python3.9/site-packages — the
user-level system install. Everything is entangled with everything.
Now the other half of the mismatch:
$ which python3
/usr/bin/python3
$ which pip
/usr/local/bin/pip
$ pip --version
pip 23.0.1 from /usr/local/lib/python3.11/site-packages/pip (python 3.11)
There it is. pip belongs to Python 3.11. python3 is 3.9. Every pip install this
colleague ran installed into a Python they were not using.
That is the entire "it's in pip list but Python can't find it" mystery, and it is the single most
common Python environment failure in the world. The fix is not a command; it is a habit —
python -m pip, always, which cannot disagree with itself.
Step 2: Find out what version of Qiskit they pinned themselves to
$ python3 -m pip show qiskit
Name: qiskit
Version: 0.45.3
Step 3 of their setup was the expensive one. They downgraded to a pre-1.0 Qiskit to match a tutorial from 2022.
This is understandable and it is backwards. It fixes one tutorial and breaks the ecosystem: current
qiskit-ibm-runtime expects Qiskit 1.0+, current documentation describes 1.0+, and every answer
they find from here on will target 1.0+. They have optimized for the past.
The right move when a tutorial fails on version grounds is to update the tutorial, not to downgrade the library — unless you specifically need to reproduce someone's published result, in which case you do it in a separate, labeled environment created for that purpose.
Step 3: Find the credential problem
$ cat quantum_config.py
IBM_TOKEN = "a3f9...c21e"
$ git log --oneline -- quantum_config.py
8a1c4d2 add config
The token is in the repository history. Whether the repository is public is not the question you should ask first, because the answer does not change what has to happen: revoke and regenerate the token now. Removing the file does not remove it from history; rewriting history does not remove it from anyone's clone or from any mirror that has already fetched it.
This is a five-second fix on the dashboard and it must not be deferred.
The Repair
Rebuild rather than patch. The environment has four independent problems, and untangling them in place costs more than starting clean.
# 1. A correct interpreter. Check what is available.
$ python3.11 --version
Python 3.11.7
# 2. A fresh, isolated environment.
$ python3.11 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python --version
Python 3.11.7
# 3. Confirm the interpreter/pip pair now agree.
(.venv) $ python -m pip --version
pip 24.0 from /home/colleague/project/.venv/lib/python3.11/site-packages/pip (python 3.11)
# 4. Install current versions, explicitly.
(.venv) $ python -m pip install --upgrade pip
(.venv) $ python -m pip install "qiskit>=1.2" qiskit-aer qiskit-ibm-runtime matplotlib pylatexenc
Then the credential, done properly — once, interactively, never in a file:
(.venv) $ python
>>> from qiskit_ibm_runtime import QiskitRuntimeService
>>> QiskitRuntimeService.save_account(token="NEW_TOKEN_FROM_DASHBOARD",
... channel="ibm_quantum_platform",
... set_as_default=True, overwrite=True)
>>> exit()
And the old file goes, with a .gitignore entry so the pattern cannot recur:
(.venv) $ git rm --cached quantum_config.py
(.venv) $ echo "quantum_config.py" >> .gitignore
(.venv) $ echo ".env" >> .gitignore
Verification
Repair is not done when the error stops. It is done when you have positive evidence that the thing works, which is a different and stronger claim.
# verify.py -- run this after any environment change
import sys
import qiskit
import qiskit_aer
import qiskit_ibm_runtime
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
print(f"interpreter {sys.executable}")
print(f"python {sys.version.split()[0]}")
print(f"qiskit {qiskit.__version__}")
print(f"qiskit-aer {qiskit_aer.__version__}")
print(f"qiskit-ibm-runtime {qiskit_ibm_runtime.__version__}")
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
sim = AerSimulator()
counts = sim.run(transpile(qc, sim), shots=1024, seed_simulator=1234).result().get_counts()
print(f"\nBell counts: {counts}")
assert set(counts) <= {"00", "11"}, f"impossible outcomes on an ideal simulator: {counts}"
assert 400 < counts.get("00", 0) < 620, "distribution is not close to 50/50"
print("simulator OK")
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
print(f"credentials OK: {len(service.backends())} backends visible")
interpreter /home/colleague/project/.venv/bin/python
python 3.11.7
qiskit 1.2.4
qiskit-aer 0.15.1
qiskit-ibm-runtime 0.30.0
Bell counts: {'00': 529, '11': 495}
simulator OK
credentials OK: 12 backends visible
Note the two assert lines. They are the difference between "it printed something" and "it printed
something correct." The first would catch a broken simulator returning garbage; the second would
catch a simulator that had somehow lost the Hadamard. Neither will ever fire — which is the point.
They cost nothing and they convert a demonstration into a test.
Analysis: What Went Wrong, Structurally
The four problems were not four unrelated accidents. They share a root cause: acting before observing.
| Symptom | Root cause | The observation that would have caught it |
|---|---|---|
| Package installed but not importable | pip and python are different installs |
sys.executable and pip --version |
| Partial, confusing failures | Python 3.9 with a library needing 3.10+ | python --version before installing |
| Half the tutorials fail | Downgraded to match one tutorial | Checking the tutorial's date rather than the library's version |
| Token in git history | Convenience over credential hygiene | Any awareness that save_account exists |
The colleague's instinct at every step was to fix — install something, downgrade something, add a file. The correct first move at every step was to find out, which takes thirty seconds and would have prevented all four.
This generalizes well beyond environments. It is the same discipline that Chapter 26 applies to quantum circuits: when a quantum program misbehaves, the instinct is to change gates, and the correct move is to find out what state the circuit is actually in.
Lessons
python -m pip install, always. One habit, one entire class of bug eliminated.- Diagnose before you install.
sys.executable,python --version,pip --version. Thirty seconds. - A virtual environment per project, from the first command. Non-negotiable in a field where frameworks disagree about NumPy.
- Never downgrade to match a tutorial. Update the tutorial. If you genuinely need an old version to reproduce a published result, make a separate labeled environment for it.
- Check the date on quantum code before you check your understanding. Half the quantum code on the internet predates Qiskit 1.0 and does not run.
- A leaked token gets revoked immediately, before any cleanup. Deleting the file does not undo the leak; regenerating the token does.
- Verify with assertions, not eyeballs. Two
assertlines turn a smoke test into a real one. - Record versions with every result. In a field where a major release deletes core functions, an undated number is unreproducible.
Questions
-
The colleague's
pippointed at Python 3.11 whilepython3was 3.9. Name two other ways this mismatch can arise on a developer machine, and give the diagnostic command for each. -
Write the
verify.pyscript for a reader who has not yet created an IBM Quantum account, so that it checks everything checkable and reports the missing credential as a clear next step rather than as a failure. -
The repair rebuilt the environment rather than patching it. Argue for patching instead. Under what circumstances is patching the right call?
-
The verification script asserts
set(counts) <= {"00", "11"}. Explain precisely why this assertion is correct for the simulator and would be wrong for hardware. Write the hardware version of the same check. -
Design a
requirements.txtfor a team of five who must reproduce each other's results exactly. What do you pin, and what do you leave floating? What are you trading away by pinning everything? -
The case study says never to downgrade to match a tutorial. Construct the strongest argument against that rule, then say what a reasonable policy would be.
-
Hardest. Write a
check_environment()function forvqelabthat a reader could run at any point in this book, which verifies the interpreter, the versions, the credential presence, and a working simulator round trip — and which reports every problem it finds rather than stopping at the first. Why does "report all problems" matter more here than in most software?