You may want to switch frameworks when targeting different hardware, when a new framework better fits your use case, or when your team standardises on a single SDK. This guide shows the same operations in Qiskit, Cirq, and PennyLane side-by-side so you can translate your existing code without guesswork, with additional notes on Braket, PyQuil, and tket as a universal compiler layer.
Common Migration Paths
Not every migration is equally common. These are the five transitions practitioners encounter most often, with the main reasons behind each.
Qiskit
→
Cirq
Moving to Google hardware (Sycamore, Willow) or needing lower-level control over qubit placement and gate timing. Cirq exposes individual qubit objects and moment structure that Qiskit abstracts away. The main cost is rewriting qubit indexing from integers to LineQubit or GridQubit objects.
Qiskit
→
PennyLane
Adding quantum machine learning, hybrid classical-quantum models, or differentiable programming. PennyLane circuits are Python functions decorated with @qml.qnode, which makes gradient computation via parameter-shift rules or autograd feel natural. Gate names are mostly one-to-one: qc.h(0) becomes qml.Hadamard(wires=0).
Qiskit
→
Braket
Deploying on AWS infrastructure or accessing hardware from IonQ, Rigetti, QuEra, and IQM through a single SDK. Braket uses a Circuit class with method chaining. Gate names differ slightly: cx becomes cnot, and parameterized circuits use FreeParameter instead of Qiskit's Parameter.
PyQuil
→
Qiskit
Rigetti's public cloud access has been curtailed and the IBM ecosystem is significantly larger. The main concept change is from Quil Program (list of Quil instructions) to Qiskit QuantumCircuit (a structured circuit object). Classical registers and measurement are handled differently: Qiskit requires explicit ClassicalRegister allocation.
Any framework
→
tket
Adding hardware-agnostic circuit optimisation without abandoning your current framework. tket sits as a compilation layer: import your Qiskit or Cirq circuit, apply optimisation passes, and submit to any supported backend. You do not need to rewrite your circuit construction logic, only add the compilation step.
Side-by-Side Code Examples
Each operation below is shown in Qiskit, Cirq, and PennyLane. All examples are runnable Python using standard library versions as of 2026.
1. Create a Bell State
A Bell state requires a Hadamard on qubit 0 followed by a CNOT. The structural difference here is that Cirq uses qubit objects while Qiskit and PennyLane use integer indices or wire labels.
import pennylane as qml
dev = qml.device('default.qubit', wires=2)
@qml.qnode(dev)
def bell_state():
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
return qml.probs(wires=[0, 1])
2. Parameterized Rotation Gate (Rx)
Parameterized circuits are handled differently in each framework. Qiskit uses a Parameter symbol bound at execution time. Cirq uses sympy symbols resolved via ParamResolver. PennyLane accepts a plain Python float directly, and autograd tracks it automatically.
Qiskit
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
theta = Parameter('theta')
qc = QuantumCircuit(1)
qc.rx(theta, 0)
# Bind the parameter before running
bound_qc = qc.assign_parameters({theta: 1.0472})
print(bound_qc)
Cirq
import cirq
import sympy
theta = sympy.Symbol('theta')
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.rx(theta)(q0))
# Resolve the symbol before simulation
resolver = cirq.ParamResolver({'theta': 1.0472})
resolved = cirq.resolve_parameters(circuit, resolver)
print(resolved)
PennyLane
import pennylane as qml
from pennylane import numpy as np
dev = qml.device('default.qubit', wires=1)
@qml.qnode(dev)
def rx_circuit(theta):
qml.RX(theta, wires=0)
return qml.expval(qml.PauliZ(0))
# Plain float works; autograd tracks it
theta_val = np.array(1.0472, requires_grad=True)
print(rx_circuit(theta_val))
print(qml.grad(rx_circuit)(theta_val))
3. Run on Simulator and Get Counts
Each framework has its own simulator API. Qiskit uses AerSimulator from qiskit-aer. Cirq's built-in Simulator uses repetitions. PennyLane returns exact probability distributions by default; use shots and qml.counts to get a sample-based histogram.
import pennylane as qml
dev = qml.device('default.qubit', wires=2)
@qml.qnode(dev, shots=1024)
def bell_counts():
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
return qml.counts(wires=[0, 1])
print(bell_counts())
4. Statevector Simulation
Getting the full statevector is useful for debugging and small-circuit analysis. Qiskit provides a Statevector class in qiskit.quantum_info. Cirq's simulate() returns a result object with final_state_vector. PennyLane returns the statevector via qml.state() as a NumPy array.
Noise models simulate realistic hardware behaviour in software. Qiskit uses NoiseModel from qiskit_aer.noise injected into AerSimulator. Cirq's DensityMatrixSimulator applies noise as channels on the circuit. PennyLane uses the default.mixed device, which works with density matrices natively.
import cirq
q0 = cirq.LineQubit(0)
# Insert depolarizing noise after each gate
noisy_circuit = cirq.Circuit(
cirq.H(q0),
cirq.depolarize(p=0.01)(q0),
cirq.measure(q0, key='m')
)
# DensityMatrixSimulator handles mixed states
sim = cirq.DensityMatrixSimulator()
result = sim.run(noisy_circuit, repetitions=1024)
print(result.histogram(key='m'))
PennyLane
import pennylane as qml
# default.mixed supports noise channels
dev = qml.device('default.mixed', wires=1)
@qml.qnode(dev)
def noisy_circuit():
qml.Hadamard(wires=0)
# Apply depolarizing channel (p total error)
qml.DepolarizingChannel(0.01, wires=0)
return qml.probs(wires=[0])
print(noisy_circuit())
# Slightly perturbed from ideal [0.5, 0.5]
6. VQE / Variational Circuit Optimisation
Variational algorithms minimise a cost function by optimising circuit parameters. Qiskit uses its Estimator primitive with SciPy's minimize. Cirq computes expectation values manually via simulate() and expectation_values_from_state_vector(). PennyLane's GradientDescentOptimizer uses parameter-shift gradients automatically.
Qiskit
import numpy as np
from scipy.optimize import minimize
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer.primitives import Estimator
theta = Parameter('theta')
qc = QuantumCircuit(1)
qc.ry(theta, 0)
# Minimise expectation of Z (ground state = -1)
observable = SparsePauliOp('Z')
estimator = Estimator()
def cost(params):
job = estimator.run([qc], [observable],
parameter_values=[params])
return job.result().values[0]
result = minimize(cost, x0=[0.1], method='COBYLA')
print('Optimal theta:', result.x)
print('Min energy:', result.fun)
import pennylane as qml
from pennylane import numpy as np
dev = qml.device('default.qubit', wires=1)
@qml.qnode(dev)
def cost_circuit(theta):
qml.RY(theta, wires=0)
return qml.expval(qml.PauliZ(0))
# Parameter-shift gradient is automatic
opt = qml.GradientDescentOptimizer(stepsize=0.4)
theta = np.array(0.1, requires_grad=True)
for step in range(50):
theta, cost = opt.step_and_cost(
cost_circuit, theta
)
print('Optimal theta:', float(theta))
print('Min energy:', float(cost))
Key Conceptual Differences
These are the mental model shifts that trip up developers most often when switching frameworks. Getting these right prevents most migration bugs.
Concept
Qiskit
Cirq
PennyLane
Qubit representation
Integer index (0, 1, 2) within a QuantumCircuit
Qubit objects: LineQubit(0), GridQubit(0,1)
Integer wire labels passed to each gate call
Circuit execution
backend.run(transpile(qc, backend)) or Sampler / Estimator primitives
cirq.Simulator().simulate(circuit) or .run(circuit, repetitions=N)
Calling the decorated @qml.qnode function like a regular Python function
Measurement
Explicit qc.measure() writes to classical bits; classical register required
cirq.measure(q, key='m'); result accessed by key name
Declared in the return statement: qml.probs(), qml.expval(), qml.sample()
Parameterisation
ParameterVector or Parameter; bound with assign_parameters()
sympy.Symbol; resolved with cirq.ParamResolver
Direct Python float or pennylane.numpy array; autograd tracks automatically
Transpilation
transpile(qc, backend) maps to device gate set and topology
Manual decomposition or cirq-google for Sycamore targets
Device plugin handles mapping; user rarely calls transpilation directly
Gradients
Via Qiskit Machine Learning; not built into the core runtime
Not built-in; manual finite differences or parameter-shift via third-party tools
First-class: parameter-shift, adjoint, and backpropagation differentiation built in
tket as a Universal Compiler Layer
tket (from Quantinuum) can sit between any high-level framework and any hardware backend. Instead of rewriting your circuits, you import them into tket, apply optimisation passes, and submit to whichever backend you need. This means you can keep writing Qiskit or Cirq code and still benefit from tket's compiler passes.
1
Write your circuit in Qiskit, Cirq, or any supported framework
→
2
Convert to tket using the appropriate extension (pytket-qiskit, pytket-cirq)
→
3
Optimise using tket's SequencePass and pass manager
→
4
Submit to any backend via tket's backend plugins
tket (via pytket-qiskit)
from qiskit import QuantumCircuit
from pytket.extensions.qiskit import qiskit_to_tk, AerBackend
from pytket.passes import (
SequencePass, FullPeepholeOptimise, DecomposeBoxes
)
# 1. Write the circuit in Qiskit as normal
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
# 2. Convert to a tket Circuit
tk_circ = qiskit_to_tk(qc)
# 3. Apply optimisation passes
optimiser = SequencePass([
DecomposeBoxes(),
FullPeepholeOptimise(),
])
optimiser.apply(tk_circ)
# 4. Submit to a backend (here: Aer via tket)
backend = AerBackend()
compiled = backend.get_compiled_circuit(tk_circ)
result = backend.run_circuit(compiled, n_shots=1024)
print(result.get_counts())
Install with: pip install pytket pytket-qiskit. For Cirq circuits use pytket-cirq and cirq_to_tk(). Backends exist for IBM, IonQ, Quantinuum, Braket, Rigetti, and more.
Braket and PyQuil Migration Notes
Migrating to Amazon Braket
Braket has its own Circuit class that is structurally similar to Qiskit but with different naming conventions and a fluent chaining API.
QuantumCircuit(n, n) becomes Circuit() (no classical bits declared upfront)
qc.cx(0, 1) becomes circuit.cnot(0, 1)
qc.rx(theta, 0) becomes circuit.rx(0, theta) (qubit index first, angle second)
Parameterized gates use FreeParameter('theta') instead of Qiskit's Parameter('theta')
Measurement is implicit; no .measure() call required for the local simulator
Circuits are run via device.run(circuit, shots=N).result()
Braket Bell state
from braket.circuits import Circuit
from braket.devices import LocalSimulator
circuit = Circuit().h(0).cnot(0, 1)
device = LocalSimulator()
result = device.run(circuit, shots=1024).result()
print(result.measurement_counts)
PyQuil uses Quil, an assembly-like instruction set. Migrating to Qiskit or Cirq means moving from a list of instruction objects to a structured circuit abstraction.
PyQuil's Program() is a list of gate and measurement instructions; Qiskit's QuantumCircuit is a graph-based circuit object
Gates are imported individually from pyquil.gates (e.g. H(0), CNOT(0, 1)); in Qiskit they are methods on the circuit object
Classical registers must be declared explicitly in Qiskit: QuantumCircuit(2, 2)
PyQuil's quilc compiler is called via get_qc(); Qiskit uses transpile(qc, backend)
PyQuil's WavefunctionSimulator corresponds to Qiskit's Statevector class
Parametric gates use p.declare('angle', 'REAL'); Qiskit uses Parameter('theta')
PyQuil Bell state (for comparison)
from pyquil import Program
from pyquil.gates import H, CNOT, MEASURE
p = Program()
ro = p.declare('ro', 'BIT', 2)
p += H(0)
p += CNOT(0, 1)
p += MEASURE(0, ro[0])
p += MEASURE(1, ro[1])
p.wrap_in_numshots_loop(1024)