A cross-framework pytest plugin for quantum program testing
Project description
pytest-quantum
A cross-framework pytest plugin for quantum program testing.
Test quantum programs the same way you test classical code, using pytest.
Works with Qiskit, Cirq, Amazon Braket, PennyLane, Graphix,
Pytket, Stim, QuTiP, Tequila, and Mitiq.
Who should use pytest-quantum?
- Quantum software engineers writing gate-model circuits in Qiskit, Cirq, PennyLane, or Pytket who need robust, reproducible unit tests.
- Researchers running VQE or QAOA who need statistically sound assertions on variational algorithm convergence and expressibility.
- Teams CI-testing quantum algorithms across multiple frameworks and backends, including noisy simulation and hardware targets.
- Hardware validation engineers characterising real devices with quantum volume, randomised benchmarking, T1/T2/T2* coherence, and XEB fidelity.
What's new in v1.0.0
- Benchmarking assertions:
assert_quantum_volume,assert_randomized_benchmarking,assert_t1_above,assert_t2_above,assert_t2star_above,assert_interleaved_rb,assert_gate_fidelity_above - Quantum ML assertions:
assert_xeb_fidelity_above,assert_expressibility_above,assert_entanglement_capability_above,assert_no_barren_plateau - Cross-platform equivalence:
assert_cross_platform_equivalent,assert_qiskit_cirq_equivalent,assert_qiskit_pytket_equivalent - Noise channel assertions:
assert_depolarizing_channel,assert_amplitude_damping_channel,assert_dephasing_channel,assert_no_leakage,assert_channel_preserves_trace,assert_channel_diamond_norm_below - Hardware assertions:
assert_backend_calibration,assert_backend_executes,assert_circuit_fits_backend,assert_mirror_fidelity,assert_real_counts_close - Mitiq error mitigation:
assert_zne_expectation_close,assert_zne_reduces_error,assert_cdr_reduces_error,assert_mitigation_improves_fidelity,assert_pec_reduces_error,assert_pec_expectation_close,assert_error_mitigation_benchmark - Sweep / variational:
assert_circuit_sweep,assert_circuit_sweep_states,assert_parametrized_unitary_continuous - Hardware fixtures:
ibm_backend,ionq_backend,quantinuum_backend,quantum_hardware_info - New fixtures:
multi_backend_runner(parallel multi-backend comparison),benchmark_suite(assertion timing) - QASM 2.0 round-trip:
assert_qasm2_roundtrip - Mid-circuit measurement detection:
assert_no_mid_circuit_measurement - qiskit-ibm-runtime is now optional (
pip install pytest-quantum[ibm])
Why pytest-quantum?
Quantum programs fail in ways classical tests don't handle:
| Problem | Without pytest-quantum | With pytest-quantum |
|---|---|---|
| Shot noise flakiness | assert counts["00"] == 512 fails ~5% of runs |
assert_measurement_distribution uses chi-square; fails only when distribution is genuinely wrong |
| Global phase | np.allclose(U1, U2) fails for physically identical states |
assert_unitary handles global phase automatically |
| Framework boilerplate | Copy-paste AerSimulator() setup in every project |
aer_simulator fixture injected automatically |
| Shot count guessing | Pick 1024 shots and hope | min_shots(epsilon=0.02) gives the statistically correct answer |
| No structure testing | No standard way to assert depth or gate counts | assert_circuit_depth, assert_circuit_width, assert_gate_count |
| No mixed-state testing | No standard way to test noisy density matrices | assert_density_matrix_close, assert_purity_above, assert_trace_distance_below |
| No hardware benchmarking | No built-in quantum volume or RB | assert_quantum_volume, assert_randomized_benchmarking, assert_t1_above |
| No cross-platform validation | Can't compare Qiskit vs Cirq circuit results | assert_cross_platform_equivalent, assert_qiskit_cirq_equivalent |
Installation
pip install pytest-quantum # core (no quantum SDK required)
pip install "pytest-quantum[qiskit]" # + Qiskit + Aer
pip install "pytest-quantum[cirq]" # + Cirq
pip install "pytest-quantum[braket]" # + Amazon Braket
pip install "pytest-quantum[pennylane]" # + PennyLane
pip install "pytest-quantum[graphix]" # + Graphix (MBQC)
pip install "pytest-quantum[ibm]" # + qiskit-ibm-runtime (real IBM hardware)
pip install "pytest-quantum[mitiq]" # + Mitiq error mitigation
pip install "pytest-quantum[cvxpy]" # + CVXPY (diamond norm / SDP noise assertions)
pip install "pytest-quantum[all]" # everything
pip install stim # + Stim (QEC)
pip install pytket # + Pytket
Quick start (Qiskit)
# test_bell.py — no conftest.py needed, fixtures are injected automatically
from pytest_quantum import assert_measurement_distribution, assert_unitary
def test_bell_distribution(aer_simulator):
from qiskit import QuantumCircuit, transpile
qc = QuantumCircuit(2)
qc.h(0); qc.cx(0, 1); qc.measure_all()
counts = aer_simulator.run(transpile(qc, aer_simulator), shots=2000).result().get_counts()
# Chi-square test: won't flake on statistical noise
assert_measurement_distribution(counts, expected_probs={"00": 0.5, "11": 0.5})
def test_hadamard_unitary():
import numpy as np
from qiskit import QuantumCircuit
qc = QuantumCircuit(1)
qc.h(0)
H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
# Global-phase-safe — e^(i*theta)*H passes too
assert_unitary(qc, H)
Quick start (PennyLane)
# test_pennylane.py
import numpy as np
from pytest_quantum import assert_state_fidelity_above
def test_rx_gate(pennylane_device):
import pennylane as qml
dev = pennylane_device(wires=1)
@qml.qnode(dev)
def rx_circuit(theta):
qml.RX(theta, wires=0)
return qml.state()
state = np.array(rx_circuit(np.pi)) # RX(π) = -iX|0⟩ = -i|1⟩
assert_state_fidelity_above(state, np.array([0, -1j]), threshold=0.99)
Quick start (Cirq)
# test_cirq.py
import math
import numpy as np
from pytest_quantum import assert_unitary
def test_hadamard_cirq():
import cirq
q = cirq.LineQubit.range(1)
circuit = cirq.Circuit(cirq.H(q[0]))
H = np.array([[1, 1], [1, -1]]) / math.sqrt(2)
assert_unitary(circuit, H)
pytest # normal suite
pytest --quantum-slow # include shot-heavy tests
pytest --quantum-shots=4000 # override shot count globally
pytest --quantum-real # enable real hardware tests (requires credentials)
Decision guide: which assertion to use?
| I want to test... | Best assertion | Alternative |
|---|---|---|
| A gate implements a specific unitary | assert_unitary |
assert_circuits_equivalent |
| Two circuits are equivalent | assert_circuits_equivalent |
assert_unitary |
| A noisy circuit's output state | assert_state_fidelity_above |
assert_trace_distance_below |
| Measurement distribution matches expected | assert_measurement_distribution |
assert_counts_close |
| Two measurement distributions are close | assert_counts_close |
assert_hellinger_close |
| A density matrix from noisy simulation | assert_density_matrix_close |
assert_trace_distance_below |
| How mixed/noisy a state is | assert_purity_above |
assert_trace_distance_below |
| Entanglement in a pure state | assert_entanglement_entropy_below |
assert_schmidt_rank_at_most |
| Single-qubit state on Bloch sphere | assert_bloch_sphere_close |
assert_states_close |
| Quantum channel is valid | assert_channel_is_cptp |
assert_process_fidelity_above |
| VQE / QAOA energy result | assert_ground_state_energy_close |
assert_expectation_value_close |
| Circuit doesn't change after refactor | assert_unitary_snapshot |
assert_distribution_snapshot |
| Circuit uses only Clifford gates | assert_circuit_is_clifford |
|
| QASM export/import preserves semantics | assert_qasm_roundtrip (v3) or assert_qasm2_roundtrip (v2) |
|
| Logical error rate of QEC code | assert_stim_logical_error_rate_below |
|
| Backend meets quantum volume spec | assert_quantum_volume |
|
| Gate fidelity from RB experiment | assert_randomized_benchmarking |
assert_interleaved_rb |
| T1 / T2 / T2* coherence times | assert_t1_above / assert_t2_above / assert_t2star_above |
|
| Cross-entropy benchmarking (XEB) | assert_xeb_fidelity_above |
|
| VQA circuit is expressive | assert_expressibility_above |
assert_entanglement_capability_above |
| No barren plateau in VQA training | assert_no_barren_plateau |
|
| Circuit compatible with hardware | assert_circuit_fits_backend |
assert_no_mid_circuit_measurement |
| Error mitigation improves results | assert_zne_reduces_error |
assert_pec_reduces_error |
All 80+ assertions
Unitary / circuit equivalence
assert_unitary(circuit, expected_matrix)
assert_circuits_equivalent(circuit_a, circuit_b)
assert_transpilation_preserves_semantics(orig, compiled)
assert_cross_platform_equivalent(circuit_a, circuit_b) # v1.0.0
assert_qiskit_cirq_equivalent(qiskit_qc, cirq_circuit) # v1.0.0
assert_qiskit_pytket_equivalent(qiskit_qc, pytket_circ) # v1.0.0
State assertions
assert_normalized(statevector)
assert_state_fidelity_above(actual, target, threshold=0.99)
assert_states_close(actual, target, atol=1e-6)
Measurement distributions
assert_measurement_distribution(counts, expected_probs)
assert_counts_close(counts_a, counts_b, max_tvd=0.05)
Density matrix assertions
assert_density_matrix_close(rho, sigma, atol=1e-6)
assert_trace_distance_below(rho, sigma, max_distance=0.01)
assert_purity_above(rho, min_purity=0.95)
assert_partial_trace_close(rho, keep_qubits, expected)
Quantum channel assertions
assert_hermitian(matrix)
assert_positive_semidefinite(matrix)
assert_commutes_with(op_a, op_b)
assert_channel_is_cptp(kraus_ops)
assert_process_fidelity_above(channel_a, channel_b, threshold=0.99)
assert_noise_fidelity_above(noisy_dm, ideal_state, threshold=0.99)
Noise channel assertions (v1.0.0)
assert_depolarizing_channel(kraus_ops, error_rate, atol=1e-6)
assert_amplitude_damping_channel(kraus_ops, gamma, atol=1e-6)
assert_dephasing_channel(kraus_ops, p_dephase, atol=1e-6)
assert_no_leakage(kraus_ops, computational_dim=2)
assert_channel_preserves_trace(kraus_ops)
assert_channel_diamond_norm_below(kraus_ops_a, kraus_ops_b, max_norm=0.01)
Entanglement assertions
assert_entanglement_entropy_below(sv, partition, max_entropy)
assert_bloch_sphere_close(sv, theta, phi, atol=0.1)
assert_schmidt_rank_at_most(sv, partition, max_rank)
Information theory
assert_hellinger_close(counts_a, counts_b, max_distance=0.1)
assert_kl_divergence_below(counts, expected_probs, max_kl=0.1)
assert_cross_entropy_below(counts, expected_probs, max_ce=1.0)
Observable / expectation value
assert_expectation_value_close(actual, expected, atol=0.01)
assert_ground_state_energy_close(actual_energy, expected_energy, atol=0.01)
assert_vqe_converges(vqe_fn, hamiltonian, target_energy, atol=0.1)
assert_cost_decreases(cost_fn, initial_params)
Qiskit Primitives
assert_sampler_distribution(sampler_result, expected_probs)
assert_estimator_close(estimator_result, expected, atol=0.01)
Circuit structure
assert_circuit_depth(circuit, max_depth=10)
assert_circuit_width(circuit, expected_qubits=3)
assert_gate_count(circuit, "cx", expected=2)
assert_gates_in_basis_set(circuit, basis_gates={"cx", "u3"})
assert_circuit_is_clifford(circuit)
assert_has_diagram(circuit, expected_diagram)
assert_no_mid_circuit_measurement(circuit) # v1.0.0
Transpilation / compilation
assert_transpilation_equivalent(circuit, backend, atol=1e-6)
assert_transpilation_depth_below(circuit, backend, max_depth=20)
assert_gate_count_after_transpilation(circuit, backend, gate, expected)
Sweeps / parametrised circuits
assert_circuit_sweep(circuit, param_values, expected_probs_list)
assert_circuit_sweep_states(circuit, param_values, expected_states)
assert_parametrized_unitary_continuous(circuit, param_range)
Snapshots / golden-file testing
assert_unitary_snapshot(circuit, name)
assert_distribution_snapshot(counts, name, max_tvd=0.05)
QASM round-trips
assert_qasm_roundtrip(circuit) # OpenQASM 3 (Qiskit) or Cirq JSON
assert_qasm2_roundtrip(circuit) # OpenQASM 2.0 (Qiskit only) — v1.0.0
QEC / Stim
assert_stim_logical_error_rate_below(circuit, max_error_rate, shots=10000)
assert_stim_detector_error_rate_below(circuit, max_error_rate, shots=10000)
assert_stabilizer_state(statevector, stabilizers)
Benchmarking (v1.0.0)
assert_quantum_volume(backend, target_qv=16, num_trials=100)
assert_randomized_benchmarking(backend, qubit=0, min_fidelity_per_clifford=0.999)
assert_t1_above(backend, qubit=0, target_t1_us=50.0)
assert_t2_above(backend, qubit=0, target_t2_us=30.0) # Hahn echo
assert_t2star_above(backend, qubit=0, target_t2star_us=20.0) # Ramsey
assert_interleaved_rb(backend, qubit=0, gate_name="X", gate_circuit=x_circ)
assert_gate_fidelity_above(backend, "cx", [0, 1], target_fidelity=0.99)
Quantum ML (v1.0.0)
assert_xeb_fidelity_above(backend, num_qubits=2, target_fidelity=0.9)
assert_expressibility_above(ansatz_fn, num_qubits=2, num_params=4, target=0.5)
assert_entanglement_capability_above(ansatz_fn, num_qubits=2, num_params=4, target=0.3)
assert_no_barren_plateau(ansatz_fn, num_qubits=4, num_params=16)
Hardware assertions (v1.0.0)
assert_backend_calibration(backend, min_t1_us=30.0, min_cx_fidelity=0.99)
assert_backend_executes(circuit, backend, shots=1024)
assert_circuit_fits_backend(circuit, backend)
assert_mirror_fidelity(backend, qubit, target_fidelity=0.95)
assert_real_counts_close(job, expected_probs, max_tvd=0.1)
Mitiq error mitigation (v0.4.0+)
assert_zne_expectation_close(circuit, observable, expected, atol=0.1)
assert_zne_reduces_error(circuit, observable, noisy_val, ideal_val)
assert_cdr_reduces_error(circuit, observable, noisy_val, ideal_val)
assert_mitigation_improves_fidelity(circuit, noisy_state, ideal_state)
assert_pec_reduces_error(circuit, observable, noisy_val, ideal_val)
assert_pec_expectation_close(circuit, observable, expected, atol=0.1)
assert_error_mitigation_benchmark(circuit, observable, methods=["zne", "cdr"])
Framework support
| Framework | Version | Fixtures | Unitary | Clifford | Gate count |
|---|---|---|---|---|---|
| Qiskit + Aer | ≥ 1.0 | aer_simulator, aer_statevector_simulator, aer_noise_simulator, qiskit_sampler, qiskit_estimator |
✓ | ✓ | ✓ |
| Cirq | ≥ 1.0 | cirq_simulator, cirq_sampler |
✓ | ✓ | ✓ |
| Amazon Braket | ≥ 1.0 | braket_simulator, braket_cloud_device |
✓ | ✓ | ✓ |
| PennyLane | ≥ 0.36 | pennylane_device |
✓ | ✓ | ✓ |
| Graphix | ≥ 0.3 | graphix_backend |
✗ | ✗ | ✗ |
| Pytket | ≥ 1.0 | pytket_circuit_factory |
✓ | ✓ | ✓ |
| Stim | ≥ 1.13 | stim_sampler |
✗ | ✗ | ✗ |
| QuTiP | ≥ 4.7 | qutip_solver |
✗ | ✗ | ✗ |
| Tequila | ≥ 1.9 | tequila_backend |
✗ | ✗ | ✗ |
| IBM Quantum | runtime ≥ 0.45 | ibm_backend |
✓ | ✗ | ✗ |
| IonQ | via qiskit-ionq | ionq_backend |
✗ | ✗ | ✗ |
| Quantinuum | via pytket-quantinuum | quantinuum_backend |
✗ | ✗ | ✗ |
Fixtures
All fixtures are auto-discovered (no imports needed) and skip automatically if the required SDK is not installed.
| Fixture | Framework | Returns |
|---|---|---|
aer_simulator |
Qiskit / Aer | AerSimulator() |
aer_statevector_simulator |
Qiskit / Aer | AerSimulator(method="statevector") |
aer_noise_simulator |
Qiskit / Aer | make_simulator(error_rate) factory |
qiskit_sampler |
Qiskit 1.0+ | StatevectorSampler() |
qiskit_estimator |
Qiskit 1.0+ | StatevectorEstimator() |
cirq_simulator |
Cirq | cirq.Simulator() |
cirq_sampler |
Cirq | run_fn(circuit, shots) callable |
braket_simulator |
Amazon Braket | LocalSimulator() |
braket_cloud_device |
Amazon Braket cloud | AwsDevice(arn) (requires --quantum-real) |
graphix_backend |
Graphix | backend with .run_pattern(pattern) |
pennylane_device |
PennyLane | make_device(wires, shots=None) factory |
pytket_circuit_factory |
Pytket | pytket.Circuit class |
stim_sampler |
Stim | sample_fn(circuit, shots) callable |
qutip_solver |
QuTiP | solve(H, psi0, tlist, c_ops) callable |
tequila_backend |
Tequila | tequila module |
ibm_backend |
IBM Quantum | IBMBackend (requires --quantum-real) |
ionq_backend |
IonQ | IonQ backend (requires --quantum-real) |
quantinuum_backend |
Quantinuum | Quantinuum backend (requires --quantum-real) |
quantum_hardware_info |
All | Dict of credential availability |
quantum_benchmark |
All | benchmark timing wrapper |
benchmark_suite |
All | assertion timing suite (v1.0.0) |
shot_budget |
All | shot counter |
multi_backend_runner |
All | parallel multi-backend runner (v1.0.0) |
quantum_shots |
All | int | None from --quantum-shots |
quantum_significance |
All | float | None from --quantum-significance |
Markers
@pytest.mark.quantum # tag as a quantum test
@pytest.mark.quantum_slow # skip unless --quantum-slow is passed
@pytest.mark.quantum_real # skip unless --quantum-real is passed (hardware tests)
@pytest.mark.shots(n=4000) # shot count hint for this test
@pytest.mark.significance(p=0.01) # p-value threshold for this test
@pytest.mark.quantum_backends("qiskit", "cirq", "pennylane") # parametrize over backends
Shot budget utilities
from pytest_quantum import min_shots, recommended_shots
n = min_shots(epsilon=0.05) # 293 shots to detect 5% TVD
n = recommended_shots({"00": 0.499, "01": 0.001, "11": 0.5}) # 5000 (driven by 0.1% outcome)
Statistical primitives
from pytest_quantum import fidelity, tvd, tvd_from_counts, chi_square_test
fidelity(psi, phi) # |<psi|phi>|^2, global-phase invariant
tvd(p, q) # Total Variation Distance (0=identical, 1=disjoint)
tvd_from_counts(counts_a, counts_b) # TVD from count dicts
chi_square_test(counts, expected_probs) # returns (statistic, p_value)
Random generators
from pytest_quantum.random import (
random_statevector, # Haar-random pure state
random_density_matrix, # random mixed state
random_unitary, # Haar-random unitary
random_kraus_channel, # random CPTP channel
depolarizing_kraus, # depolarizing channel Kraus operators
)
CLI options
| Option | Description |
|---|---|
--quantum-slow |
Run quantum_slow-marked tests (skipped by default) |
--quantum-real |
Run quantum_real-marked tests against real hardware |
--quantum-shots N |
Override shot count for all tests |
--quantum-significance P |
Override p-value threshold globally |
--quantum-update-snapshots |
Regenerate all snapshot files |
Academic citation
If you use pytest-quantum in research, please cite it:
@software{ghatule2026pytest_quantum,
title = {pytest-quantum: A cross-framework pytest plugin for quantum program testing},
author = {Ghatule, Tejas},
year = {2026},
url = {https://github.com/qbench/pytest-quantum},
version = {1.0.0}
}
See CITATION.cff for full metadata.
Contributing
See CONTRIBUTING.md for setup, test commands, code style, and PR checklist.
git clone https://github.com/qbench/pytest-quantum
cd pytest-quantum
uv sync --all-extras --group dev
uv run pytest # 646+ tests
uv run ruff check src/ tests/
uv run mypy src/
License
MIT. See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_quantum-1.0.0.tar.gz.
File metadata
- Download URL: pytest_quantum-1.0.0.tar.gz
- Upload date:
- Size: 387.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe042adb6c5ec50039f1ccf70fec7640cafcc878ae5e1a661bc78ed58bd22f93
|
|
| MD5 |
c7d3131ac3aa2a87965187bf32cf0a56
|
|
| BLAKE2b-256 |
eb2ff1500ec2fd5102bd4ed0fafa68a7f97d6cd3df6b846599eb5a892eb55818
|
Provenance
The following attestation bundles were made for pytest_quantum-1.0.0.tar.gz:
Publisher:
publish.yml on qbench/pytest-quantum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_quantum-1.0.0.tar.gz -
Subject digest:
fe042adb6c5ec50039f1ccf70fec7640cafcc878ae5e1a661bc78ed58bd22f93 - Sigstore transparency entry: 1154988591
- Sigstore integration time:
-
Permalink:
qbench/pytest-quantum@be42a5fef05412551b4fad7ac1754293a9731148 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/qbench
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@be42a5fef05412551b4fad7ac1754293a9731148 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pytest_quantum-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pytest_quantum-1.0.0-py3-none-any.whl
- Upload date:
- Size: 107.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7013c3819b5e2aded8298737a85b285eb597c3737c01bc127308d6c5d3ca30a
|
|
| MD5 |
2fa1f65d5b8ea4c34b3b477405d3ebe7
|
|
| BLAKE2b-256 |
744399f4a324e14a54d2f432e5699873bf98948cc993945ae46d344a8cc5cfea
|
Provenance
The following attestation bundles were made for pytest_quantum-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on qbench/pytest-quantum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_quantum-1.0.0-py3-none-any.whl -
Subject digest:
f7013c3819b5e2aded8298737a85b285eb597c3737c01bc127308d6c5d3ca30a - Sigstore transparency entry: 1154988592
- Sigstore integration time:
-
Permalink:
qbench/pytest-quantum@be42a5fef05412551b4fad7ac1754293a9731148 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/qbench
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@be42a5fef05412551b4fad7ac1754293a9731148 -
Trigger Event:
push
-
Statement type: