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 — with pytest.
Works with Qiskit, Cirq, Amazon Braket, PennyLane, Graphix,
Pytket, and Stim.
What's new in v0.3.0
- Pytket and Stim support:
pytket_circuit_factory,stim_samplerfixtures + 3 QEC assertions - Quantum channel assertions:
assert_channel_is_cptp,assert_process_fidelity_above,assert_noise_fidelity_above,assert_hermitian,assert_positive_semidefinite,assert_commutes_with - Entanglement assertions:
assert_entanglement_entropy_below,assert_bloch_sphere_close,assert_schmidt_rank_at_most - Information-theoretic distribution tests:
assert_hellinger_close,assert_kl_divergence_below,assert_cross_entropy_below - Random state/circuit generators (
pytest_quantum.random):random_statevector,random_density_matrix,random_unitary,random_kraus_channel,depolarizing_kraus - OpenQASM round-trip testing:
assert_qasm_roundtrip - QEC assertions for Stim circuits:
assert_stim_logical_error_rate_below,assert_stim_detector_error_rate_below,assert_stabilizer_state assert_normalized: validate statevector has unit normassert_has_diagram: text diagram comparison for Qiskit/Cirq/Pytket circuitsassert_transpilation_preserves_semantics: Qiskit transpile + equivalence check
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 — only fails 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 |
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[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
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 |
— |
| Logical error rate of QEC code | assert_stim_logical_error_rate_below |
— |
| Matrix is Hermitian | assert_hermitian |
— |
| Two operators commute | assert_commutes_with |
— |
| Statevector is normalized | assert_normalized |
— |
All 38 assertions
Unitary / circuit equivalence
assert_unitary(circuit, expected_matrix) # verifies circuit implements this unitary
assert_circuits_equivalent(circuit_a, circuit_b) # two circuits are equivalent (cross-framework)
assert_transpilation_preserves_semantics(orig, compiled) # transpilation is semantics-preserving
State assertions
assert_normalized(statevector) # ||ψ||₂ = 1 (v0.3.0)
assert_state_fidelity_above(actual, target) # |⟨actual|target⟩|² ≥ threshold
assert_states_close(actual, target, atol=1e-6) # elementwise, up to global phase
Measurement distributions
assert_measurement_distribution(counts, expected_probs) # chi-square goodness-of-fit
assert_counts_close(counts_a, counts_b, max_tvd=0.05) # Total Variation Distance
Density matrix assertions (v0.2.0)
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 (v0.3.0)
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)
Entanglement assertions (v0.3.0)
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 (v0.3.0)
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 (v0.2.0)
assert_expectation_value_close(actual, expected, atol=0.01)
assert_ground_state_energy_close(actual_energy, expected_energy, atol=0.01)
Qiskit Primitives (v0.2.0)
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_circuit_is_clifford(circuit)
assert_has_diagram(circuit, expected_diagram) # (v0.3.0)
Snapshots / golden-file testing (v0.2.0)
assert_unitary_snapshot(circuit, name)
assert_distribution_snapshot(counts, name, max_tvd=0.05)
OpenQASM round-trip (v0.3.0)
assert_qasm_roundtrip(circuit)
QEC / Stim (v0.3.0)
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)
Framework support
| Framework | Version | Fixtures | assert_unitary | assert_circuit_is_clifford | assert_gate_count |
|---|---|---|---|---|---|
| Qiskit + Aer | ≥ 1.0 | aer_simulator, aer_statevector_simulator, aer_noise_simulator, qiskit_sampler, qiskit_estimator |
yes | yes | yes |
| Cirq | ≥ 1.0 | cirq_simulator, cirq_sampler |
yes | yes | yes |
| Amazon Braket | ≥ 1.0 | braket_simulator |
yes | yes | yes |
| PennyLane | ≥ 0.36 | pennylane_device |
yes | yes | yes |
| Graphix | ≥ 0.3 | graphix_backend |
— | — | — |
| Pytket | ≥ 1.0 | pytket_circuit_factory |
yes | yes | yes |
| Stim | ≥ 1.13 | stim_sampler |
— | — | — |
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() |
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 |
quantum_benchmark |
All | benchmark wrapper |
shot_budget |
All | shot counter |
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.shots(n=4000) # shot count hint for this test
@pytest.mark.significance(p=0.01) # p-value threshold for this test
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 (v0.3.0)
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-shots N |
Override shot count for all tests |
--quantum-significance P |
Override p-value threshold globally |
--quantum-update-snapshots |
Regenerate all snapshot files |
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 # 401+ 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-0.4.0.tar.gz.
File metadata
- Download URL: pytest_quantum-0.4.0.tar.gz
- Upload date:
- Size: 300.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1087f09a01a7be8a7f986680ae4ee55925dacd80dc6537d610326dff23db5df0
|
|
| MD5 |
e1d6396c7fc11f1a245afd79342eaf22
|
|
| BLAKE2b-256 |
b680e7779709f1d3f61ae4efbf36739eb523a1093e67e927a419f85562fdf389
|
Provenance
The following attestation bundles were made for pytest_quantum-0.4.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-0.4.0.tar.gz -
Subject digest:
1087f09a01a7be8a7f986680ae4ee55925dacd80dc6537d610326dff23db5df0 - Sigstore transparency entry: 1154472971
- Sigstore integration time:
-
Permalink:
qbench/pytest-quantum@98ba06a19e0f54e5fa15eca8705dc795b5a4c8e7 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/qbench
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@98ba06a19e0f54e5fa15eca8705dc795b5a4c8e7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pytest_quantum-0.4.0-py3-none-any.whl.
File metadata
- Download URL: pytest_quantum-0.4.0-py3-none-any.whl
- Upload date:
- Size: 70.8 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 |
ed655dfec46f3090285a9bf88196076d837e49f7897ebfcb102cfe2177c748e3
|
|
| MD5 |
ac807f8beb46e3499b10cd610361ea0f
|
|
| BLAKE2b-256 |
ad25c5aa2a8311b1652b8af0cdde29cdb1dec5d0b40cf84113da089c8c23e9ce
|
Provenance
The following attestation bundles were made for pytest_quantum-0.4.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-0.4.0-py3-none-any.whl -
Subject digest:
ed655dfec46f3090285a9bf88196076d837e49f7897ebfcb102cfe2177c748e3 - Sigstore transparency entry: 1154472975
- Sigstore integration time:
-
Permalink:
qbench/pytest-quantum@98ba06a19e0f54e5fa15eca8705dc795b5a4c8e7 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/qbench
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@98ba06a19e0f54e5fa15eca8705dc795b5a4c8e7 -
Trigger Event:
push
-
Statement type: