Migrating from Qiskit 0.x to Qiskit 1.0
A practical guide to porting Qiskit 0.x code to Qiskit 1.0: deprecated APIs removed, primitives V2 migration, backend changes, and transpiler updates.
Circuit diagrams
Qiskit 1.0, released in February 2024, is the first stable release of the Qiskit SDK under the new unified package structure. It removes a large number of APIs that were deprecated in the 0.x series, unifies the package namespace, and ships the V2 primitives interface as the standard way to run circuits.
If you have code written for Qiskit 0.44 or earlier, this guide covers the changes you will encounter and how to update.
Installation Change
The biggest structural change is the package name:
# Old (Qiskit 0.x - meta-package that pulled in multiple packages)
pip install qiskit
# This installed: qiskit-terra, qiskit-aer, qiskit-ibm-runtime, etc. as a bundle
# New (Qiskit 1.0 - unified SDK, separate runtime package)
pip install qiskit
pip install qiskit-aer # local simulation
pip install qiskit-ibm-runtime # IBM Quantum access
In Qiskit 1.0, qiskit is the core SDK (formerly qiskit-terra). The old qiskit-terra package is gone. Most imports from qiskit still work the same, but some submodule paths changed.
Import Path Changes
# OLD - Qiskit 0.x
from qiskit.providers.aer import AerSimulator # from qiskit-aer inside the bundle
from qiskit.providers.fake_provider import FakeMontreal
from qiskit.opflow import PauliSumOp, StateFn # REMOVED in 1.0
# NEW - Qiskit 1.0
from qiskit_aer import AerSimulator # separate package
from qiskit_ibm_runtime.fake_provider import FakeMontreal # separate package
from qiskit.quantum_info import SparsePauliOp # replacement for PauliSumOp
Primitives: V1 to V2
This is the most significant code change. The V1 primitives (Sampler, Estimator) were deprecated in 0.x and are removed (or non-functional) in modern Qiskit IBM Runtime. V2 primitives use a new input format called a PUB (Primitive Unified Bloc).
Sampler Migration
# Requires: qiskit_ibm_runtime
# OLD - Qiskit 0.x Sampler V1
from qiskit.primitives import Sampler
sampler = Sampler()
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
job = sampler.run([qc], shots=1000)
result = job.result()
quasi_dists = result.quasi_dists # V1 result format
print(quasi_dists[0]) # {0: 0.498, 3: 0.502} (integer bitstrings)
# NEW - Qiskit 1.0 SamplerV2
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
sampler = StatevectorSampler()
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
# PUB format: (circuit,) or (circuit, param_values, shots)
job = sampler.run([(qc,)], shots=1000)
result = job.result()
pub_result = result[0] # index by PUB
counts = pub_result.data.meas.get_counts() # classical register name 'meas'
print(counts) # {'00': 498, '11': 502}
Key changes:
- Input is a list of PUBs, each a tuple
(circuit,)or(circuit, param_values, shots) - Result accessed by index:
result[0] - Classical register data accessed via
result[0].data.<register_name> get_counts()on the BitArray returns a dict of bitstring counts- No more
quasi_dists; useget_counts()orget_bitstrings()
Estimator Migration
# Requires: qiskit_ibm_runtime
# OLD - Qiskit 0.x Estimator V1
from qiskit.primitives import Estimator
from qiskit.opflow import PauliSumOp
estimator = Estimator()
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
op = SparsePauliOp("ZZ")
job = estimator.run([qc], [op])
result = job.result()
print(result.values[0]) # V1: result.values[i]
# NEW - Qiskit 1.0 EstimatorV2
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
estimator = StatevectorEstimator()
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
op = SparsePauliOp("ZZ")
# PUB: (circuit, observables)
job = estimator.run([(qc, op)])
result = job.result()
print(result[0].data.evs) # V2: result[0].data.evs
Key changes:
- V1
result.values[i]becomes V2result[i].data.evs - V1
result.metadata[i]['variance']becomes V2result[i].data.stds ** 2 - Observables can be passed as
SparsePauliOp, list, or dict
Parameterized Circuit Migration
# Requires: qiskit_ibm_runtime
# OLD - separate lists of circuits, observables, parameter values
from qiskit.circuit import ParameterVector
params = ParameterVector('theta', 2)
qc = QuantumCircuit(2)
qc.ry(params[0], 0)
qc.ry(params[1], 1)
job = estimator.run(
[qc, qc], # one circuit per job element
[op, op],
[[0.1, 0.2], [0.3, 0.4]] # parallel parameter sets
)
# NEW - single PUB with 2D parameter array
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
params = ParameterVector('theta', 2)
qc = QuantumCircuit(2)
qc.ry(params[0], 0)
qc.ry(params[1], 1)
estimator = StatevectorEstimator()
op = SparsePauliOp("ZZ")
param_vals = np.array([[0.1, 0.2], [0.3, 0.4]]) # shape (n_sets, n_params)
job = estimator.run([(qc, op, param_vals)]) # one PUB handles all sets
result = job.result()
print(result[0].data.evs.shape) # (2,) - one value per parameter set
Backend Changes: BackendV2
In Qiskit 0.x, backends implemented BackendV1. In Qiskit 1.0, BackendV2 is the standard. The interface changes:
# Requires: qiskit_ibm_runtime
# OLD - BackendV1 interface
backend = provider.get_backend('ibm_nairobi')
config = backend.configuration()
n_qubits = config.n_qubits
coupling_map = config.coupling_map
basis_gates = config.basis_gates
props = backend.properties()
t1 = props.t1(qubit=0)
# OLD transpile call
from qiskit import transpile
qc_t = transpile(qc, backend=backend, optimization_level=3)
# NEW - BackendV2 interface
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.backend('ibm_brisbane') # BackendV2
n_qubits = backend.num_qubits # direct attribute, no .configuration()
coupling_map = backend.coupling_map # CouplingMap object
basis_gates = backend.operation_names # list of strings
# Properties via target
target = backend.target
t1_qubit_0 = target.qubit_properties[0].t1 # may be None if not calibrated
# Transpile -- same API
from qiskit import transpile
qc_t = transpile(qc, backend=backend, optimization_level=3)
Removed APIs
These are commonly used 0.x APIs that no longer exist in 1.0:
| Old API | Replacement |
|---|---|
from qiskit.opflow import PauliSumOp | from qiskit.quantum_info import SparsePauliOp |
from qiskit.opflow import StateFn | Use circuits directly with primitives |
result.quasi_dists | result[0].data.<register>.get_counts() |
result.values (Estimator) | result[0].data.evs |
backend.configuration() | backend.num_qubits, backend.coupling_map, etc. |
backend.properties() | backend.target.qubit_properties[i] |
QuantumCircuit.qasm() | qiskit.qasm2.dumps(qc) or qiskit.qasm3.dumps(qc) |
QuantumCircuit.from_qasm_str(s) | qiskit.qasm2.loads(s) or qiskit.qasm3.loads(s) |
execute(qc, backend) | Use primitives (Sampler/Estimator) or backend.run() |
execute() Migration
The execute() function from qiskit.execute_function was removed. The preferred path is the primitives. For raw circuit execution:
# OLD
from qiskit import execute
job = execute(qc, backend, shots=1024)
counts = job.result().get_counts()
# NEW with Aer (local)
from qiskit_aer import AerSimulator
sim = AerSimulator()
from qiskit import transpile
qc_t = transpile(qc, sim)
job = sim.run(qc_t, shots=1024)
counts = job.result().get_counts()
# NEW with IBM Runtime Sampler (hardware or simulator)
from qiskit_ibm_runtime import SamplerV2 as Sampler
sampler = Sampler(backend)
job = sampler.run([(qc,)], shots=1024)
counts = job.result()[0].data.meas.get_counts()
Error Suppression and Mitigation
Noise mitigation APIs moved entirely to qiskit_ibm_runtime:
# OLD - various mitigation packages scattered around
# from qiskit.ignis.mitigation import CompleteMeasFitter # REMOVED (qiskit-ignis deprecated)
# NEW - mitigation via Runtime options
from qiskit_ibm_runtime import SamplerV2 as Sampler, Options
options = Options()
options.resilience_level = 1 # T-REx (Twirled Readout Error eXtinction)
# options.resilience_level = 2 # ZNE (Zero Noise Extrapolation)
sampler = Sampler(backend, options=options)
Quick Checklist
If you are porting 0.x code to 1.0, go through this list:
- Update
pip install qiskit(the package is now the full SDK, no more terra) - Move
from qiskit.providers.aer import ...tofrom qiskit_aer import ... - Move
from qiskit.providers.fake_provider import ...tofrom qiskit_ibm_runtime.fake_provider import ... - Replace V1 Sampler/Estimator with
StatevectorSampler/StatevectorEstimatorfor local runs - Update result access:
result.quasi_dists[i]becomesresult[i].data.meas.get_counts() - Update result access:
result.values[i]becomesresult[i].data.evs - Replace
backend.configuration().n_qubitswithbackend.num_qubits - Replace
QuantumCircuit.qasm()withqiskit.qasm2.dumps(qc) - Replace
execute(qc, backend)with primitives orbackend.run(transpile(qc, backend)) - Remove any
qiskit-ignisimports; use RuntimeOptions.resilience_levelinstead
The migration is mostly mechanical: the quantum circuit API itself (QuantumCircuit, gates, transpilation) changed very little. The heavy lifting is in primitives and backend interfaces.
Was this tutorial helpful?