I lost an evening to four lines of code that were completely correct. That is the part that stung. Not a typo, not a logic error, nothing I could have spotted by reading harder. The lines were correct. They were just correct for a version of Qiskit that no longer exists.
If you are learning quantum in 2026, this will happen to you, probably more than once. So here is the trap, laid out, with the time I already paid so you do not have to.
The evening I lost
I was following along with a tutorial. Clean, well written, the kind that makes you feel like you are in good hands. It built a small circuit and ran it like this:
from qiskit import execute
job = execute(qc, backend, shots=1024)
result = job.result()
counts = result.get_counts()
I ran it. It died on the first line.
ImportError: cannot import name 'execute' from 'qiskit'
So I did what you do. I assumed it was me. I checked my spelling. I reinstalled things. I read the line ten times. I went looking for what I had broken in my environment, because the code was obviously right, it was right there in a tutorial, so the problem had to be on my end. It was not on my end. The function that whole example is built around, execute, does not exist in modern Qiskit. It was removed. The tutorial was not wrong when it was written. It was just written for a different era, and nothing on the page told me that.
That is the trap in one sentence. The code is not buggy. It is expired.
Why quantum has a worse version problem than most
Every library changes over time. This one changed hard. Qiskit 1.0, in early 2024, was a deliberate clean break, and a lot of the old, comfortable ways of doing things were swept out in it and the versions after. I am on 2.x now, and whole patterns that filled tutorials two years ago simply do not run anymore.
The reason it bites harder in quantum than in, say, web development is volume. When a popular web framework changes, the internet floods with new tutorials within weeks, and the old ones sink. Quantum is a smaller world. There is far less content overall, the old material does not get buried, and it keeps showing up at the top of your search results and inside the AI you ask for help, looking exactly as authoritative as it did the day it was correct. So you reach for the obvious example, and the obvious example is from before the break.
I wrote a whole separate post about not letting an AI tutor feed you dead code. This is that dead code, in the flesh. Here is exactly what to write instead.
What is actually dead, and what replaced it
A short, honest map of what I have hit so far on 2.x.
execute is gone. The single entry point everyone used to run a circuit was removed. There is no drop in rename. The whole flow around it changed.
Aer and BasicAer no longer hang off the main package. Code that starts with Aer.get_backend('qasm_simulator') assumes a convenience that modern Qiskit does not give you from the top level. The high performance simulator now lives in its own package, qiskit_aer, which you install and import on its own.
The run and get_counts flow was replaced by primitives. Instead of one execute call, you now go through a small object called a primitive. There are two you will meet constantly. A Sampler, when you want measurement outcomes, the counts. An Estimator, when you want an expectation value, that single summary number. They are the modern front door, and once you know they exist, the new examples stop looking foreign.
The shape of the result changed too. The old result.get_counts() became a short chain that reaches into the result and pulls the counts out of a named register. That is the part that surprises people even after they fix the imports, so it gets its own section below.
The modern minimal pattern
Here is the smallest circuit that actually runs on a current Qiskit, end to end, with nothing deprecated in it. This is the version of those four tutorial lines that does not expire on you.
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
qc = QuantumCircuit(1, 1) # one qubit, one classical bit to store the result
qc.h(0) # put it in superposition
qc.measure(0, 0) # measure qubit 0 into classical bit 0
sampler = StatevectorSampler()
result = sampler.run([qc], shots=1024).result()
counts = result[0].data.c.get_counts()
print(counts) # {'0': ~512, '1': ~512}
Run it. You get roughly five hundred of each, the equal superposition you would expect. Notice the shape. You hand the sampler a list of circuits, because you can run several at once. You get back a result you index into with [0] for the first circuit. And the counts come out of result[0].data.c. That .c is the name of your classical register, and it is the single most common thing people trip on after they fix the imports, so here it is on its own.
The gotcha that gets everyone: which register name
When you read the counts, you have to name the classical register you measured into, and the name depends on how you created it. This is not in most old tutorials, because the old flow did not work this way.
If you build the circuit with an explicit classical register, as above with QuantumCircuit(1, 1) and measure(0, 0), the register is called c by default. So you read result[0].data.c.get_counts().
But there is a popular shortcut, measure_all(), that adds measurements to every qubit in one call. It is convenient, and it creates its own register, named meas. So if you used it, the same line becomes result[0].data.meas.get_counts(), with meas instead of c.
qc = QuantumCircuit(1)
qc.h(0)
qc.measure_all() # convenient, but it names the register 'meas'
result = StatevectorSampler().run([qc], shots=1024).result()
counts = result[0].data.meas.get_counts() # .meas, not .c
print(counts)
Run it. Same outcome, different attribute name. Get the name wrong and you get an error that does not obviously say register name, so you sit there confused. Now you know. The name you read is the name you created. If you did not name it, it is c with an explicit register, and meas with measure_all.
Sampler gives you counts, Estimator gives you a number
While we are here, it is worth knowing the second primitive, because half the modern examples use it and it confused me at first. The Sampler above gives you measurement outcomes, the counts dictionary. But often you do not want the full distribution, you want a single summary number, the expectation value from the measurement post. That is what the Estimator is for. You hand it a circuit and an observable, the thing you want the average of, and it gives you back one number.
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
qc = QuantumCircuit(1)
qc.h(0) # |+>, which leans neither way
observable = SparsePauliOp("Z") # measure the Z expectation
result = StatevectorEstimator().run([(qc, observable)]).result()
print(float(result[0].data.evs)) # 0.0
Run it. You get zero, exactly the balanced result for |+⟩ that the Born rule post worked out by hand. Notice there is no measure on the circuit and no counts. The Estimator handles the averaging for you and returns the number directly. So the rule of thumb is simple. Want the outcomes, the histogram? Sampler, with a measurement on the circuit. Want one expectation value? Estimator, with an observable and no measurement. Mixing those two up is a common early stumble, and now it is not one for you.
On real hardware, your circuit gets translated first
One more piece of the modern flow that the old execute call hid from you. A real quantum computer does not understand every gate you write. Each device has its own small set of gates it can physically perform, its native set, and your circuit has to be rewritten into only those gates before it can run. That rewriting is called transpilation, and in modern Qiskit you do it explicitly.
from qiskit import QuantumCircuit, transpile
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
# rewrite the circuit using only this device's native gates
native = transpile(qc, basis_gates=["rz", "sx", "x", "cx"], optimization_level=1)
print(sorted({instr.operation.name for instr in native.data})) # ['cx', 'rz', 'sx']
Run it. The Hadamard you wrote is gone, replaced by a combination of the device's actual gates that does the same thing. This is not optional on real hardware. The transpiler also tries to optimise the circuit and to map your abstract qubits onto the physical ones, which on a noisy machine matters a great deal. The old execute did some of this silently. The modern flow asks you to do it on purpose, which is more code but far less mystery, because you can see exactly what your circuit became before it ran.
How I actually found the fix
It is worth walking the diagnosis, because the process generalises to every expired pattern you will ever hit, not just this one. When the ImportError came up, the move that finally worked was boring and methodical.
First, I read the error literally instead of panicking. Cannot import name execute from qiskit means exactly what it says: that name is not there. Not misspelled, not misconfigured, not there. That alone ruled out half of what I had been checking. Second, I printed my version, the one line from earlier, and confirmed I was on 2.x, well past the break. Third, instead of searching the open web, where I would just find more of the same expired tutorials, I went to the official migration guide and searched for execute. It told me directly that the function was removed and pointed me at the primitives that replaced it. Ten minutes, start to finish, once I stopped flailing and followed the trail.
The lesson is not about this one function. It is the routine. Read the error as fact, confirm the version, go to the versioned docs, find what the old thing maps to. That sequence turns a confusing wall into a quick lookup, and it works for every removed function you will meet, because the migration guides are written precisely to answer the question "this used to exist, what do I do now."
How to stop losing evenings
The fixes are boring and they work.
Print your version, always. One line, import qiskit then print(qiskit.__version__), at the top of anything you are debugging. Half of these mysteries evaporate the moment you know exactly which Qiskit you are talking to. I now do it reflexively, the way I check which environment a deploy is pointing at before I panic about a broken pipeline.
Pin it. Write the version down in your project so it does not silently move under you between sessions. Future you, three weeks from now, wondering why the same code behaves differently, will be grateful.
Run it before you trust it. This is the same rule from the AI post, and it is the one that actually saves you. A correct looking example proves nothing. A running example proves everything. The interpreter does not care how authoritative the source looked.
Go to the versioned source, not the first result. The official documentation is versioned and kept current with the library, so it ages far better than a two year old blog post or a top voted answer from another era. A random blog post is not versioned, and an AI answer is dated to whenever it was trained. When something will not run, I go to the official docs first now, check that the page matches the version I printed, and only widen the search if the official material does not cover what I need. That one habit quietly removes most of the expired code from your life, because you stop pulling it in to begin with.
Deprecation is a warning. Removal is a wall.
One distinction worth carrying, because the two feel different and you handle them differently.
A deprecation warning means the thing still works, for now, but it is on its way out. Your code runs, and a yellow message tells you to stop using that function before a future version deletes it. Treat those warnings as a to do list, not noise. The version that turns them red is coming.
A removal is the wall I hit. The thing is simply gone, and you get a hard error, today, with no grace period. execute is past the warning stage. It is a wall now. The lesson is to act on deprecation warnings while they are still just warnings, so you are never the person discovering a removal the hard way on a deadline.
The discipline you already have
Here is the thing. As a builder, I already live this lesson everywhere else. I pin dependencies. I check which version is running before I assume the logic is wrong. I know that "it worked in the tutorial" means nothing if the tutorial is from another year. None of this was new wisdom. I just forgot, for one evening, that quantum is software, and software rots, and a fast moving young library rots faster than most.
So I treat Qiskit the way I treat every other dependency in every other system I have ever shipped. Know the version. Trust the running code, not the screenshot. Read the changelog. The physics is the exotic part. The version trap is just ordinary engineering, and you already know how to handle it. You just have to remember that it applies here too.
If you carry one model out of this, make it this. Modern Qiskit has a small, stable shape: you build a circuit as an object, you transpile it for wherever it is going to run, and you execute it through a primitive, a Sampler for outcomes or an Estimator for an expectation value, then you read the result out of a named register. Build, transpile, run through a primitive, read. Every current example fits that shape, and anything that does not, anything reaching for execute or pulling Aer off the top level package, is from before the break and will fight you. Once the shape is in your head, you can spot expired code on sight, which is a quieter superpower than it sounds. You stop copying the broken examples in the first place, and you stop losing evenings to code that was never going to run.