Qiskit Intermediate Free 5/61 in series 20 min read

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.

What you'll learn

  • Qiskit
  • migration
  • primitives
  • BackendV2
  • SamplerV2
  • EstimatorV2
  • transpilation

Prerequisites

  • Python proficiency
  • Beginner quantum computing concepts (superposition, entanglement)
  • Linear algebra basics

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; use get_counts() or get_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 V2 result[i].data.evs
  • V1 result.metadata[i]['variance'] becomes V2 result[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 APIReplacement
from qiskit.opflow import PauliSumOpfrom qiskit.quantum_info import SparsePauliOp
from qiskit.opflow import StateFnUse circuits directly with primitives
result.quasi_distsresult[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 ... to from qiskit_aer import ...
  • Move from qiskit.providers.fake_provider import ... to from qiskit_ibm_runtime.fake_provider import ...
  • Replace V1 Sampler/Estimator with StatevectorSampler/StatevectorEstimator for local runs
  • Update result access: result.quasi_dists[i] becomes result[i].data.meas.get_counts()
  • Update result access: result.values[i] becomes result[i].data.evs
  • Replace backend.configuration().n_qubits with backend.num_qubits
  • Replace QuantumCircuit.qasm() with qiskit.qasm2.dumps(qc)
  • Replace execute(qc, backend) with primitives or backend.run(transpile(qc, backend))
  • Remove any qiskit-ignis imports; use Runtime Options.resilience_level instead

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?