OpenQASM 3 and Qiskit Interoperability
How to convert between Qiskit QuantumCircuit objects and OpenQASM 3 strings for sharing, archiving, and cross-platform workflows.
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 tooling. Quantinuum’s pytket stack imports OpenQASM 2 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 was deprecated and then removed in Qiskit 1.0; older tutorials may still show it. Use the qasm2 and qasm3 modules instead:
# Old way (OpenQASM 2, removed in Qiskit 1.0)
# 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 (qasm2.dumps()) | 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 toolchain is built around pytket, whose QASM loader reads OpenQASM 2 (including Quantinuum’s hqslib1 extensions), not QASM 3:
from pytket.qasm import circuit_from_qasm_str
# Export QASM 2 from Qiskit for the pytket path
from qiskit import qasm2
qasm2_str = qasm2.dumps(qc)
# Load into pytket
tket_circuit = circuit_from_qasm_str(qasm2_str)
# From here, submit to Quantinuum backends via pytket-quantinuum
If a circuit only exists as QASM 3, round-trip it through Qiskit first (qasm3.loads followed by qasm2.dumps). Check the pytket changelog for current QASM coverage, as it evolves by version.
Amazon Braket
Amazon Braket uses OpenQASM 3.0 as its native program format and can load QASM 3 circuits directly:
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, measurements, and parameter inputs, but not every QASM 3 feature is available on every device (advanced classical control flow, in particular, varies by target). For circuits that use only basic gates, this path works well. For more complex circuits, test the import carefully. Note that Braket does not consume QASM 2; convert older files to QASM 3 first.
Cirq (Google)
Cirq’s circuit_from_qasm importer reads OpenQASM 2 fully and also has limited, experimental support for OpenQASM 3 (classical control flow, arbitrary gate definitions, and some gates without a one-to-one Cirq representation are not yet handled):
import cirq
# circuit_from_qasm handles QASM 2 fully; QASM 3 coverage is partial
# Export from Qiskit as QASM 2 for the most reliable path
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, though simple QASM 3 circuits may import directly.
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)
Recent PennyLane releases also include a qml.from_qasm3 loader for OpenQASM 3 programs, with feature coverage that is still expanding. 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 (incl. hqslib1 extensions) | Not supported (use QASM 2) |
| Amazon Braket | Not supported | Supported (native format, subset of features) |
| Cirq | Full | Partial (experimental, via circuit_from_qasm) |
| PennyLane | Full | Partial (via qml.from_qasm3) |
When sharing circuits across most SDKs, QASM 2 remains the safest lowest-common-denominator format, with Amazon Braket as the notable exception since it consumes QASM 3 natively. 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 produced QASM 2 output and was removed in Qiskit 1.0, so code copied from older tutorials will fail with an AttributeError. New code should always use the qasm3 module:
# Wrong: .qasm() was removed in Qiskit 1.0
# 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 removed 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?