OpenQASM 3 and Qiskit Interoperability
How to convert between Qiskit QuantumCircuit objects and OpenQASM 3 strings for sharing, archiving, and cross-platform workflows.
Circuit diagrams
Why Convert Between Qiskit and OpenQASM 3
Qiskit is a Python SDK for building and running quantum circuits. OpenQASM 3 is a text-based language for describing quantum programs. They represent the same thing (quantum circuits) in different formats, and you will frequently need to move between them.
Common reasons to convert:
- Sharing circuits with collaborators: Your collaborator uses PennyLane, not Qiskit. Exporting your circuit to QASM lets them import it into their framework without installing Qiskit or rewriting anything. The
.qasmfile acts as a neutral exchange format that both sides can read. - Cross-platform execution: You developed a circuit locally in Qiskit, but you want to run it on Quantinuum hardware through their portal. Quantinuum accepts OpenQASM 3 directly, so you export to QASM and submit without going through Qiskit’s runtime at all.
- Archival and reproducibility: Two years from now, the Qiskit API may have changed, and a pickled
QuantumCircuitobject may not load cleanly. A.qasmfile is a stable, human-readable record of a circuit that does not depend on any particular SDK version. You can always reconstruct the circuit from the text. - Importing published circuits: A research paper on arXiv includes a QASM listing for a variational ansatz. Instead of manually rebuilding the circuit gate by gate from the paper’s figure, you copy the QASM text and load it directly into Qiskit with a single function call.
- Continuous integration and testing: Your team commits
.qasmfiles alongside test scripts in version control. A CI pipeline loads each file, runs it on a simulator, and checks that the output distribution matches expected results. The circuit definitions stay SDK-independent, so switching from Qiskit to Cirq on the runner side requires changing only the loader.
Exporting a Qiskit Circuit to OpenQASM 3
Use qiskit.qasm3.dumps() to convert a QuantumCircuit to an OpenQASM 3 string:
from qiskit import QuantumCircuit
from qiskit import qasm3
# Build a simple Bell state circuit
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
# Export to OpenQASM 3
qasm_string = qasm3.dumps(qc)
print(qasm_string)
This produces output like:
OPENQASM 3.0;
include "stdgates.inc";
bit[2] c;
qubit[2] q;
h q[0];
cx q[0], q[1];
c[0] = measure q[0];
c[1] = measure q[1];
To write directly to a file, use qasm3.dump() with a file handle:
with open("bell_state.qasm", "w") as f:
qasm3.dump(qc, f)
Understanding the QASM 3 Output
If you have not seen QASM syntax before, the exported output can look unfamiliar. Here is what each line means in the Bell state example above:
OPENQASM 3.0; is the version declaration. It must be the first non-comment line in every QASM 3 file. Parsers use this to determine which grammar rules to apply, so omitting it or writing OPENQASM 2.0; will cause QASM 3 loaders to reject the file.
include "stdgates.inc"; imports the standard gate library. This file defines common gates like h, cx, x, z, s, t, and others. Without this include, the parser does not know what h or cx means, and the file will fail to load. You do not need to provide the stdgates.inc file yourself; QASM 3 parsers bundle it.
bit[2] c; declares a classical register named c with 2 bits. Note that Qiskit’s QASM 3 exporter uses the bit keyword, not the older creg keyword from QASM 2. The bit type is the QASM 3 way to declare classical storage.
qubit[2] q; declares a quantum register named q with 2 qubits. Similarly, Qiskit outputs qubit instead of the QASM 2 keyword qreg. If your Qiskit circuit has named registers (e.g., QuantumRegister(3, name='data')), those names appear in the QASM output.
h q[0]; applies the Hadamard gate to qubit 0. This is the standard QASM 3 gate syntax: the gate name, followed by the target qubit(s), followed by a semicolon. Multi-qubit gates list their arguments separated by commas.
cx q[0], q[1]; applies a CNOT gate with q[0] as control and q[1] as target. The argument ordering matches the standard convention: control first, target second.
c[0] = measure q[0]; measures qubit 0 and stores the result in classical bit 0. This assignment syntax is new in QASM 3. In QASM 2, the same operation was written with an arrow: measure q[0] -> c[0];. The QASM 3 syntax reads more naturally as “the classical bit receives the measurement result.”
Importing OpenQASM 3 into Qiskit
Use qasm3.loads() to parse an OpenQASM 3 string back into a QuantumCircuit:
from qiskit import qasm3
qasm_source = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c[0] = measure q[0];
c[1] = measure q[1];
"""
qc = qasm3.loads(qasm_source)
print(qc.draw())
To load from a file:
qc = qasm3.load("bell_state.qasm")
Loading a Circuit from a Research Paper
Suppose you find a QASM 3 circuit listing in a paper or a GitHub repository. Here is the full workflow to get it running in Qiskit.
First, save the QASM string to a file. You can do this manually, or inline it in your script:
from qiskit import qasm3
from qiskit.primitives import StatevectorSampler
# QASM 3 circuit from a paper (a simple GHZ state preparation)
qasm_from_paper = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
h q[0];
cx q[0], q[1];
cx q[1], q[2];
c[0] = measure q[0];
c[1] = measure q[1];
c[2] = measure q[2];
"""
# Load the QASM into a Qiskit QuantumCircuit
qc = qasm3.loads(qasm_from_paper)
# Inspect the circuit to confirm it matches the paper
print(qc.draw())
Output:
┌───┐ ┌─┐
q_0: ┤ H ├──■───────┤M├──────
└───┘┌─┴─┐ └╥┘┌─┐
q_1: ─────┤ X ├──■───╫─┤M├───
└───┘┌─┴─┐ ║ └╥┘┌─┐
q_2: ──────────┤ X ├─╫──╫─┤M├
└───┘ ║ ║ └╥┘
c: 3/═════════════════╩══╩══╩═
0 1 2
Now run it on a simulator:
# Run on the StatevectorSampler
sampler = StatevectorSampler()
job = sampler.run([qc], shots=1024)
result = job.result()
# Get the counts
counts = result[0].data.c.get_counts()
print(counts)
# Expected: roughly equal counts for '000' and '111'
If the paper provides the QASM as a file, download it and load directly:
qc = qasm3.load("path/to/paper_circuit.qasm")
This approach saves time and avoids transcription errors compared to rebuilding circuits by hand from a figure.
Round-Trip Verification
A useful test when working with QASM interop is to verify that a circuit survives a round trip (export then re-import) without changing its behavior:
from qiskit import QuantumCircuit, qasm3
from qiskit.quantum_info import Operator
# Original circuit
original = QuantumCircuit(2)
original.h(0)
original.cx(0, 1)
original.s(1)
# Round trip: Qiskit -> QASM 3 -> Qiskit
qasm_str = qasm3.dumps(original)
reconstructed = qasm3.loads(qasm_str)
# Compare the unitary matrices
op_original = Operator(original)
op_reconstructed = Operator(reconstructed)
print(f"Unitaries equal: {op_original.equiv(op_reconstructed)}")
If the unitaries match, the round trip preserved the circuit’s quantum operation exactly.
Exporting with Parameters
Parameterized circuits are common in variational algorithms like VQE and QAOA. When you export a parameterized circuit to QASM 3, the parameter names appear as input variables in the output.
from qiskit import QuantumCircuit, qasm3
from qiskit.circuit import Parameter
theta = Parameter('theta')
phi = Parameter('phi')
qc = QuantumCircuit(2)
qc.ry(theta, 0)
qc.ry(phi, 1)
qc.cx(0, 1)
# Export with unbound parameters
qasm_str = qasm3.dumps(qc)
print(qasm_str)
The output includes input declarations for each parameter:
OPENQASM 3.0;
include "stdgates.inc";
input float[64] theta;
input float[64] phi;
qubit[2] q;
ry(theta) q[0];
ry(phi) q[1];
cx q[0], q[1];
The input keyword in QASM 3 marks these as values that must be supplied at runtime. This is useful for sharing ansatz templates: your collaborator receives the circuit structure and can bind their own parameter values later.
If you bind the parameters before exporting, the QASM output contains concrete numeric values instead:
# Bind parameters to specific values
bound_qc = qc.assign_parameters({theta: 1.57, phi: 0.78})
qasm_str_bound = qasm3.dumps(bound_qc)
print(qasm_str_bound)
The bound output replaces the input declarations with literal numbers:
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
ry(1.57) q[0];
ry(0.78) q[1];
cx q[0], q[1];
Choose unbound export when sharing a circuit template. Choose bound export when sharing a specific instance of a circuit that is ready to run.
Decomposing Before Export
Some Qiskit instructions cannot be directly expressed in QASM 3. These include:
Initialize: Sets qubits to a specific statevector. This is a high-level Qiskit operation that the QASM 3 exporter does not know how to serialize.- 3-qubit and higher gates: Gates like
CCXGate(Toffoli) are supported, but custom multi-qubit unitaries defined withUnitaryGatehave no direct QASM 3 representation. - Custom Python gates: If you define a gate as a Python class, the exporter cannot convert arbitrary Python logic into QASM text.
Attempting to export these gates without decomposition raises an error. The cleanest solution is to use transpile with optimization_level=0 to decompose the circuit into standard gates before exporting:
from qiskit import QuantumCircuit, qasm3, transpile
from qiskit.circuit.library import UnitaryGate
import numpy as np
# A circuit with a custom unitary gate
qc = QuantumCircuit(2)
qc.h(0)
# Apply a custom 2-qubit unitary
unitary_matrix = np.array([
[1, 0, 0, 0],
[0, 0, 1, 0],
[0, 1, 0, 0],
[0, 0, 0, 1]
]) / 1 # SWAP matrix
qc.append(UnitaryGate(unitary_matrix), [0, 1])
# This would fail: qasm3.dumps(qc)
# Decompose to standard gates first
decomposed = transpile(qc, basis_gates=['u', 'cx'], optimization_level=0)
print(decomposed.draw())
# Now export successfully
qasm_str = qasm3.dumps(decomposed)
print(qasm_str)
Using optimization_level=0 tells the transpiler to decompose without rearranging or optimizing the circuit. This preserves the structure as closely as possible while converting everything to standard gates. If you also want the transpiler to simplify redundant gates, use optimization_level=1 or higher, but be aware that this may change the circuit structure significantly.
The basis_gates argument controls which gates appear in the output. Common choices:
['u', 'cx']: Universal gate set, works everywhere['h', 'cx', 's', 't']: Clifford+T, common in error correction literature['rx', 'ry', 'rz', 'cx']: Rotation-based, common in variational algorithms
Pick the basis set that matches your target platform or your audience’s conventions.
Differences from OpenQASM 2
Qiskit previously used OpenQASM 2 via the .qasm() method on QuantumCircuit. This older interface is deprecated in favor of the qasm3 module. Key differences:
# Old way (OpenQASM 2, deprecated)
# qasm2_string = qc.qasm()
# New way (OpenQASM 3)
qasm3_string = qasm3.dumps(qc)
Practical differences between QASM 2 and QASM 3 export:
| Feature | QASM 2 (.qasm()) | QASM 3 (qasm3.dumps()) |
|---|---|---|
| Header | OPENQASM 2.0; | OPENQASM 3.0; |
| Classical types | creg c[2]; | bit[2] c; |
| Quantum registers | qreg q[2]; | qubit[2] q; |
| Measurement syntax | measure q[0] -> c[0]; | c[0] = measure q[0]; |
| Classical control | Limited if | Full if/else, while, for |
| Custom gates | Supported | Supported, plus modifiers |
What Gets Preserved and What Does Not
When exporting to QASM 3, the gate operations and measurements are preserved faithfully. However, some Qiskit-specific metadata does not survive the conversion:
Preserved:
- Gate operations and their parameters
- Qubit and classical bit registers
- Measurement operations
- Barrier instructions
- Classical conditionals (if statements)
Not preserved:
- Circuit name and metadata
- Qiskit-specific instructions that have no QASM 3 equivalent (e.g.,
Initialize,UnitaryGatewithout decomposition) - Layout and transpilation information
- Custom Python-defined gates (must be decomposed first)
If your circuit contains non-standard instructions, decompose them before exporting:
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import Unroll3qOrMore, Decompose
# Decompose high-level gates before QASM export
pm = PassManager([Decompose(), Unroll3qOrMore()])
decomposed = pm.run(qc)
qasm_str = qasm3.dumps(decomposed)
Working with Other Platforms
QASM 3 is increasingly supported across quantum computing platforms, making it a practical interchange format. Here is how to move circuits between Qiskit and other major tools.
Quantinuum via pytket
Quantinuum’s hardware accepts QASM 3, and the pytket library provides a convenient loader:
from pytket.qasm import circuit_from_qasm_str
# Start with a QASM 3 string exported from Qiskit
qasm_str = qasm3.dumps(qc)
# Load into pytket
tket_circuit = circuit_from_qasm_str(qasm_str)
# From here, submit to Quantinuum backends via pytket-quantinuum
The pytket parser handles most standard QASM 3 constructs. If you use advanced QASM 3 features like classical loops or subroutines, verify that pytket supports them, as coverage varies by version.
Amazon Braket
Amazon Braket has partial support for loading QASM 3 circuits:
from braket.circuits import Circuit
# Load a QASM 3 string into Braket
braket_circuit = Circuit.from_ir(qasm_str)
Braket’s QASM 3 support covers standard gates and measurements but does not yet handle all QASM 3 features (such as classical control flow or parameterized inputs). For circuits that use only basic gates, this path works well. For more complex circuits, test the import carefully.
Cirq (Google)
Cirq does not have a built-in QASM 3 importer as of early 2026. It does support QASM 2 import:
import cirq
# Cirq supports QASM 2, not QASM 3
# Export from Qiskit as QASM 2 if targeting Cirq
from qiskit import qasm2
qasm2_str = qasm2.dumps(qc)
cirq_circuit = cirq.contrib.qasm_import.circuit_from_qasm(qasm2_str)
If you need to share circuits with Cirq users, exporting as QASM 2 (using qiskit.qasm2.dumps()) is currently the most reliable path.
PennyLane (Xanadu)
PennyLane can load QASM 2 strings via its from_qasm function:
import pennylane as qml
qasm2_str = qasm2.dumps(qc)
dev = qml.device("default.qubit", wires=2)
loaded_circuit = qml.from_qasm(qasm2_str)
PennyLane’s QASM 3 support is under active development. Check the latest PennyLane release notes for current status.
Summary of Platform QASM Support
| Platform | QASM 2 | QASM 3 |
|---|---|---|
| Qiskit | Full (via qiskit.qasm2) | Full (via qiskit.qasm3) |
| pytket / Quantinuum | Full | Supported (most features) |
| Amazon Braket | Full | Partial |
| Cirq | Full | Not yet supported |
| PennyLane | Full | In development |
When sharing circuits across platforms, QASM 2 remains the safest lowest-common-denominator format. Use QASM 3 when your target platform explicitly supports it and you need QASM 3 features like parameterized inputs or classical control flow.
Common Mistakes
Using qc.qasm() instead of qasm3.dumps(qc)
The .qasm() method on QuantumCircuit produces QASM 2 output and is deprecated. New code should always use the qasm3 module:
# Wrong: produces QASM 2, deprecated
# old_qasm = qc.qasm()
# Correct: produces QASM 3
from qiskit import qasm3
new_qasm = qasm3.dumps(qc)
If you specifically need QASM 2 output (for example, to share with a platform that only supports QASM 2), use the explicit qasm2 module instead of the deprecated method:
from qiskit import qasm2
qasm2_str = qasm2.dumps(qc)
Loading QASM 2 with the QASM 3 parser
The qasm3.loads() function expects QASM 3 syntax. If you pass it a QASM 2 string, it fails because the two formats have different grammars. Always match the parser to the format:
from qiskit import qasm2, qasm3
# QASM 2 string (note: uses creg/qreg and arrow syntax)
qasm2_source = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
measure q[0] -> c[0];
"""
# Wrong: this will raise a parse error
# qc = qasm3.loads(qasm2_source)
# Correct: use the QASM 2 parser
qc = qasm2.loads(qasm2_source)
Check the header line (OPENQASM 2.0; vs OPENQASM 3.0;) to determine which parser to use. If you are writing a utility that handles both formats, read the first line of the string and dispatch accordingly.
Exporting circuits with unsupported gates
If your circuit contains Initialize, UnitaryGate, or other Qiskit-specific instructions, the QASM 3 exporter raises an error because these instructions have no standard QASM 3 representation. Decompose first:
from qiskit import transpile
# Circuit with an Initialize instruction
qc = QuantumCircuit(2)
qc.initialize([0, 1, 0, 0], [0, 1]) # Initialize to |01>
# This raises an error:
# qasm3.dumps(qc)
# Fix: decompose to standard gates
decomposed = transpile(qc, basis_gates=['u', 'cx'], optimization_level=0)
qasm_str = qasm3.dumps(decomposed)
Assuming all stdgates.inc gates work everywhere
The QASM 3 standard gate library (stdgates.inc) defines dozens of gates. However, each hardware platform supports only a small native gate set. A circuit that uses h, cx, t, and s will export to valid QASM 3, but the target hardware may only support rz, sx, and cx. The QASM file is syntactically correct, yet the hardware backend rejects it.
The solution is to transpile to your target platform’s native gates before exporting:
from qiskit import transpile
# Transpile to a specific backend's native gate set
transpiled = transpile(qc, basis_gates=['rz', 'sx', 'cx'], optimization_level=1)
qasm_str = qasm3.dumps(transpiled)
This produces QASM 3 output that uses only the gates your target hardware actually supports.
A Practical Workflow
Here is a common pattern for teams that use multiple quantum SDKs:
- Build and test circuits in Qiskit (or any SDK with QASM 3 export)
- Export to
.qasmfiles and commit them to version control - On the execution side, load the
.qasmfile into whichever SDK targets your hardware - Results flow back through your analysis pipeline regardless of which SDK ran the circuit
This keeps your circuit definitions portable and SDK-independent, while letting each team member use the tools they prefer.
Was this tutorial helpful?