Skip to content

I could not find a place to practise Qiskit, so I built one

Every Qiskit tutorial I found was written for a version that no longer runs. So I built twenty exercises that check your answer by inspecting the objects your code produces.

The qx list command showing twenty exercises grouped into four acts, each row carrying its number, slug, title and status, under a progress bar. A demo course used to exercise every command.
The twenty exercises, grouped into four acts. The statuses come from a demo course used to exercise every command, not from my own progress.

For the last few weeks I have been looking for a workbook.

Not a tutorial, and not documentation. A workbook. Somewhere I could open a file, write an answer, run one command, and be told whether the answer was right. The thing that exists for almost every other subject I have ever tried to learn.

I did not find one.

There is no shortage of Qiskit material. IBM's documentation is a reference, and a very good one. There are textbooks that teach the theory properly. There are hundreds of tutorials. What I could not find was the part in the middle: a set of exercises that run against the Qiskit that ships today, and that check your answer instead of showing you theirs.

So I built one. Twenty exercises in four acts, from an empty laptop to a Bell state on a real IBM machine, and then to the inequality that no shared coin can reach. It is called quantum-exercises, it is MIT licensed, and it lives on GitHub and PyPI if you would rather go straight there.

Writing the exercises turned out to be the easy half.

Every tutorial I found was written for software that no longer exists

In February 2024, Qiskit 1.0 removed a large part of the interface that every piece of teaching material had been built on.

execute() is gone. It was the centre of every tutorial: build a circuit, call it, get results. It has two replacements now, transpilefollowed by backend.run, or the primitives.

Aer left the qiskit package. The line from qiskit import Aer is an ImportError today. It lives in qiskit-aer now.

IBMQ no longer exists. It became QiskitRuntimeService, with a completely different authentication model.

The shape of a result changed. With the primitives, counts moved to result[0].data.<register>.get_counts(), a level of indexing that was not there before. The lower level path still returns a Result that has get_counts() on it, and almost every tutorial gets that distinction wrong in one direction or the other.

I wrote about what broke and what replaced it after it cost me an evening, so I will not repeat that here. The technical consequence is boring: old code does not run.

The human consequence is not boring at all. A beginner following a tutorial from 2021 hits an ImportError on line three. They have no way of knowing the instructions are wrong rather than them. The conclusion most people draw is that they are not clever enough for quantum computing, when what actually happened is that they were reading instructions for software that no longer exists.

That is the observation the whole thing is built on. It is also why exercise 13 is called "Code from 2021 that no longer runs" and teaches migration explicitly. It is the one skill that unlocks every old tutorial you will ever find.

The nearest things exist, and neither one is a workbook

The obvious question is whether somebody has already done this, and the honest answer is that two projects come close.

The first is Microsoft's Quantum Katas. The original repository was archived by its owner on 21 August 2024 and is read only, which is easy to find and easy to misread. The Katas were not abandoned. The experience was rebuilt and now lives at quantum.microsoft.com, with the modern kit in Microsoft's qsharp repository. It teaches Q#, a different language on a different stack.

The second is much closer, and I did not know it existed until somebody pointed me at it. In May 2026, Juan Cruz-Benito and Ismael Faro at IBM Research published Qiskit QuantumKatas: a port of those same exercises from Q# to Qiskit, 350 tasks across 26 categories, each with a prompt, a canonical solution and deterministic verification. So somebody did build the Katas for Qiskit, and it would be dishonest of me to claim the field was empty.

It is aimed at a different reader. It is a benchmark for evaluating large language models on quantum code generation, shipped as a dataset, and the thing being measured is a model rather than a person. There is no install, no progression, and no feedback written for somebody who is stuck, because none of that would serve what it is for.

So the gap I could not fill is narrower than "nobody built this", and the precise version is the only one worth stating: a local, self checking beginner workbook for the Qiskit that ships today, with feedback written in the language of the concept you tripped over, and an optional path onto real hardware.

I do not know enough to guarantee the physics myself

Here is the problem with what I have just described.

This site says plainly that I am learning quantum computing. I am not a physicist. I do not have the standing to publish twenty exercises and ask you to trust that the physics in them is right because I say so.

So I did not.

What I built instead turns every assumption behind a lesson into something executable. Every reference solution is run. Every diagnostic an exercise promises is triggered on purpose and asserted, so a message that stops being accurate breaks a test instead of quietly misleading somebody. The suite runs against the current release of Qiskit on a schedule, so a version that breaks an exercise surfaces on the repository rather than in your terminal.

Be clear about what that does and does not buy. It does not certify the physics independently. If an assumption of mine is wrong, a test built on that assumption enshrines the error rather than catches it. What it does is make every assumption explicit, executable and open to anybody who wants to check it, and it catches the far more common failure, which is drift: material that was right when it was written and stopped being right when the library moved underneath it.

That is a weaker claim than "these exercises are correct". It is also one I can stand behind, and expert eyes on the checkers are welcome.

Right now that machinery is 1,131 tests, at 100 percent line and branch coverage of the tool, with the threshold set to fail below 100. That coverage figure is about the code that does the checking, not about the exercises themselves. One test is gated behind a real hardware account and does not run by default. The rest run on Python 3.10 through 3.14.

Comparing text is the wrong way to check a quantum answer

The naive way to check an exercise is to compare the learner's output to the reference output. For quantum code that is wrong twice over, and both reasons are interesting.

The first is global phase.

The states |ψ⟩ and e^(iθ)|ψ⟩ are the same physical state. No experiment can tell them apart, ever. It is not that the difference is small, it is that the difference is not observable in principle. A learner who prepares the right state with the phase turned by π has not made a mistake, because there is nothing there to be mistaken about.

So states are compared with Statevector.equiv, which ignores global phase, and operators with Operator.equiv, for the same reason. Punishing somebody for a quantity that does not exist would be a strange way to teach them physics.

The second reason is that there is rarely one right circuit.

Exercise 08 asks you to build a controlled Z out of nothing but Hadamards and a CNOT. There are several orders that work. Comparing your code to mine would reward the one I happened to write. Comparing the operator your code produces to the operator the exercise wants rewards all of them.

# checks.py, simplified
from qiskit.quantum_info import Operator, Statevector

ATOL = 1e-6

def assert_state(actual, target):
    if not Statevector(actual).equiv(Statevector(target), atol=ATOL):
        raise CheckFailed("Your circuit does not prepare the target state.")

def assert_operator(actual, target):
    if not Operator(actual).equiv(Operator(target), atol=ATOL):
        raise CheckFailed("Your circuit is not the target operator.")

The tolerance is 1e-6, looser than the Qiskit default of 1e-8, so that an honest answer assembled in a slightly different gate order still passes. There is no correct solution. There is a correct operator.

If the reason gates behave like matrices at all is not obvious to you, I wrote about why they are matrices you can multiply by hand.

Sampling is random, so equality is the wrong test

Measurement results are worse than ambiguous. They are random.

Run the same correct circuit twice and you get different counts. Any check that compares counts for equality is not strict, it is simply wrong, and it will fail correct answers on a schedule set by chance.

So counts are never compared for equality. Three things happen instead.

Proportions are tested against the binomial standard error. For an outcome expected with probability p over N shots, SE = sqrt(p(1-p)/N), and the observed proportion has to fall within four standard errors. That threshold gives roughly a 6.3 in 100,000 chance of failing a correct answer, per assertion, which is low enough that the suite does not flake.

Whole distributions go through a chi square goodness of fit test at a significance level of 0.001.

Support is checked only where the physics forbids an outcome outright, meaning which results appear at all rather than how often. Never on hardware, where noise puts a few shots almost anywhere.

There is a detail here that took me a while to appreciate. For a two outcome distribution, the chi square statistic is exactly z squared, so the 3.3 standard error threshold coming out of the chi square test and the 4 sigma threshold from the proportion test are consistent with each other rather than two arbitrary numbers. Exercise 06 explains that to the learner, next to the Born rule worked out on paper first.

The consequence of all of this is the property I actually wanted. An answer that is right for reasons I did not anticipate passes. An answer that only looks right does not.

One exercise accepted an answer that never asked the question

That property failed once, and finding out how it failed was the most interesting hour of the build.

Exercise 10 is Deutsch's algorithm. You are handed an oracle and you have to determine, in a single query, whether the function it encodes is constant or balanced. The whole point is the single query.

There is a way to cheat. Read the oracle's matrix, work out by hand which of the four possible functions it is, and return the answer without ever putting the oracle in a circuit. The check accepted it.

My first conclusion was that this could not be fixed, and I had an argument for it: for two of the four oracles, the circuit a cheat produces is identical to the honest one, bit for bit. If the outputs are the same, no inspection of the output can separate them.

The argument was wrong, and I only saw why when I was asked to look again. I had quietly assumed the check could only inspect the output. It also controls the input.

The fix is to hand the function an oracle from outside the set of four: a T gate, which is not a phase oracle for any boolean function at all. Code that composes whatever it is given still works. Code that classifies the matrix against a table of four known cases falls over. I tested it against eight implementations. Three honest styles pass, five varieties of cheating and failure are rejected cleanly.

When you cannot tell two behaviours apart by their output, change the input. That generalises well beyond quantum computing, and it is the single thing from this project I expect to reuse most.

Two holes remain, and both are worth stating rather than hiding.

The probe proves that the oracle reaches the circuit. It does not count queries. A solution that applies the oracle nine times still passes, because each of the four real oracles is its own inverse and T to the ninth power is T again, so an odd number of applications is indistinguishable from one. Nobody writes that by accident and it gains you nothing, but a single query is the claim the algorithm makes, and the checker does not enforce it yet.

The second is narrower. An implementation written specifically to detect the probe, hardcoding the four known matrices and composing correctly for anything else, also passes. I verified that. But I verified the part that matters too: for the four real oracles, the circuit it produces is physically identical to the honest one. What stays undetectable there is exactly what is unobservable.

A Bell state on a real machine, on 11 August 2026

Exercise 14 is where the simulator stops. You transpile a Bell pair to a backend's actual instruction set and you send it to a QPU.

Exercise 14 run on ibm_marrakesh, a real IBM QPU. The queue question is answered yes, then the histogram shows 1024 shots: 00 at 49.0 percent, 11 at 48.4 percent, and 01 and 10 together at 2.54 percent. The summary reports the circuit as submitted, its ISA form, and that the disagreeing shots are noise rather than a bug.
Exercise 14 on ibm_marrakesh, 11 August 2026. The 01 and 10 rows are outcomes an ideal Bell state cannot produce.

Nothing reaches hardware without somebody answering a question first. The tool asks IBM which QPU is free, tells you how many jobs are ahead of you, and waits. If there is no terminal to answer in, so a script, a CI job or an editor task, the answer is no and it says why. Watch mode always answers no, because it runs again on every save and asks nothing. If the queue cannot be read, nothing is sent.

Here is the run from the screenshot above. 1,024 shots on ibm_marrakesh, on 11 August 2026:

00 appeared 502 times and 11 appeared 496 times. Together that is 998 shots out of 1,024, or 97.46 percent, and those are the two outcomes an ideal Bell state is allowed to produce.

01 appeared 14 times and 10 appeared 12 times. Together, 26 shots, 2.54 percent. Theory forbids both of them. The machine produced them anyway.

Those 26 shots sit where the device's published error rates say they should, rather than where a broken circuit would put them, and the runner says so on screen rather than leaving you to worry about it. They are the subject of the next exercise, which is about reading a noisy result honestly instead of assuming you broke something. If the reason the allowed outcomes are correlated at all is not settled in your head, I wrote about entanglement without the spooky part.

The IBM Quantum Platform workload page for job d9tmg21dsedc73aj5ma0, showing a completed sampler job on ibm_marrakesh, created and finished on 11 August 2026, with eleven seconds total completion time and two seconds of QPU usage.
A screenshot of my own terminal is not evidence. This is the job record behind it, on IBM's side.

Three durations appear in those two pictures and they are not the same thing. The command took 21.03 seconds end to end. The job took 11 seconds at IBM. The QPU itself was busy for 2 seconds. On the open plan, where the budget is 10 minutes per rolling 28 days, the difference between those numbers is worth understanding before you spend any of it.

The transpilation is visible in the summary too. The circuit goes in as one Hadamard, one CNOT and two measurements. It comes out as six rz, three sx, one cz, two measurements and a barrier, because that is the instruction set ibm_marrakeshexposes. Not all of those are things the machine does. A barrier is a directive to the compiler, and on this hardware an rz is usually a frame change rather than a pulse, which is to say it takes no time at all. This is the same wall I ran into the first time I put a circuit on IBM hardware from an automation workflow.

The course checks itself on the 1st and the 15th

The obvious objection to everything above is that Qiskit will change again, and then this becomes one more set of exercises that no longer runs. That objection is correct, and it is the reason the project exists, so it would be absurd not to answer it.

A scheduled job answers exactly one question: does this course still work with the Qiskit that exists today? It has four parts, and they do not all run on the same day.

Latest ignores the lockfile on purpose and resolves the newest versions the ranges allow. It is the only job that can see a change out in the world, and it runs on both the 1st and the 15th.

Preview installs above the major version ceiling, because the release most likely to break something is precisely the one Latest is not permitted to see. It runs twice a month as well, and it reports without failing the workflow, because the badge answers whether the shipped course works and the shipped course still pins below the next major.

Locked installs the exact pinned versions on Linux, macOS and Windows. The 1st only, because nothing it tests changes inside a month and it costs more than the rest put together.

Wheel builds the package and walks a learner through installing it, on all three operating systems. The 1st only, and by hand before a release worth being sure about. Note what it is not: it builds from the repository rather than downloading what is on PyPI, so it tests the packaging rather than the artefact somebody already has.

The honest version of what this buys is narrower than nothing can break. A Qiskit release can land on the 2nd and go unseen until the 15th. What the schedule removes is the exact failure this project exists because of, which is material that quietly stops running while nobody finds out for two years. This is the same discipline that turned up six defects in my n8n node after I had already shipped it, applied to a different kind of artefact.

This is an on ramp, not a curriculum

Twenty exercises stop well short of quantum computing.

There is no Grover, no Shor, no VQE or QAOA, and no error correction beyond correcting readout by hand. Act IV reaches expectation values and the CHSH inequality, and then it ends. If you want a full course, this is not one.

What it is meant to do is get you from an empty laptop to the point where the rest of the material becomes readable. You need to be able to write basic Python, and not very much of it. You need no quantum background and no linear algebra beyond multiplying a small matrix by a vector, and where a matrix shows up, the runner prints it.

I will add exercises as I learn the material well enough to be checked on it. I am not going to promise a schedule, because I have a physically demanding day job and a linter that is only at 0.1. What I will promise is narrower and I can keep it: anything I add goes through the same checking as everything already there, or it does not go in.

One more design note, visible in the screenshot at the top of this post. One row reads solved rather than done. That is what the tool records when the answer was revealed rather than found. qx solution prints the answer to anybody who asks for it, because a system built to stop you cheating yourself is a system built on a misunderstanding of who it is for. It just keeps the distinction visible instead of flattening it into a green tick.

Where it lives

It is on PyPI and on GitHub, MIT licensed.

uv tool install quantum-exercises
qx init
cd quantum-exercises
qx doctor
qx next

pipx and pip work too. An IBM Quantum account is optional: every exercise runs on a local simulator without one, and the only exercise that wants real hardware degrades to a noise model copied from a real device, and tells you that is what happened.

The repository is at github.com/TuguiDragos/quantum-exercises and the package at pypi.org/project/quantum-exercises.

If something breaks, or an error message leaves you stuck, the issue tracker is the useful place to put it. There are fourteen rules that turn common Qiskit exceptions into plain language, and every one of them was written by triggering the real error rather than guessing at the wording. The rules only cover the mistakes I could anticipate. The long tail is the part I cannot, and when nothing matches, the tool says so and calls it its own gap rather than yours.

That is the whole thing. I could not find a workbook, so there is one now.