qxlint’s icon: two sliders, a lavender bar, and a check mark

Static checks for Qiskit Primitives V2.

It reads your code. It never runs it.

qxlint finds the mistakes the move from V1 to V2 primitives introduced: counts read off the wrong object, a V1 field on a V2 result, a circuit sampled without a measurement, and a channel a release has removed.

uvx qxlint .

Python 3.11 to 3.14 · Qiskit optional · Free, MIT License

Two things fail quietly in Primitives V2 code. Calling get_counts() one level too high raises, but only after the job has run. And a circuit with no measurement instruction can come back as all zeros, which looks like a physics result. Both can be decided from the source, without a model, a network, or a quantum computer.

Get the highlights.

15 rules.

11 for Python files and notebooks, and 4 for circuits in memory, 2 of them in preview.

Reads. Never runs.

It never imports or executes your code, makes no network requests, and needs no quantum hardware.

Tests.

Run in CI on Python 3.11, 3.12, 3.13 and 3.14, with Qiskit, without it, and at its lowest version.

Coverage.

Of statements and branches, held by a CI gate rather than reported as a number.

API checks.

Its model of Qiskit, checked against a real install on the 1st and 15th of every month.

Rules

Each one says when it is wrong.

Every rule page documents when the pattern it catches is legitimate. If that cannot be written, the rule does not ship. Pick a rule to see what it flags, what it leaves alone, and what qxlint printed for each.

QXL0 · Parsing

QXL1 · Results and circuits

QXL2 · Runtime and API

QXL3 · Circuits in memory

QXL000

Source could not be parsed

  • default tier
  • error

Why it is a problem

The file is not valid Python for the interpreter running qxlint, or the notebook is not valid nbformat JSON. Nothing downstream can run. An interpreter cannot parse syntax newer than itself, so code using 3.14 only syntax needs qxlint to run on 3.14.

When it is legitimate

Never as a finding to ignore, though a file that is intentionally not Python should be excluded rather than suppressed.

Flagged
def f(
$ qxlint example.pyexample.py:1:6: QXL000 cannot parse: '(' was never closed
Not flagged
def f():
    pass
$ qxlint example.pyNo findings

QXL101

get_counts() called on a container that does not have it

  • default tier
  • error

Why it is a problem

In the Primitives V2 result shape, counts live on a BitArray, not on the result, the pub result, or the data bin. Calling get_counts() on any of those raises AttributeError at runtime. The shape is PrimitiveResult → PubResult → .data (DataBin) → <classical register> (BitArray). The register is named by the circuit, so it is not always meas.

When it is legitimate

Never on these receivers. get_counts() is correct and common on a BitArray, and on a legacy backend.run(...).result(), and this rule fires on neither. It also stays silent whenever the receiver type cannot be proven.

Flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler

qc = QuantumCircuit(2)
qc.measure_all()
result = StatevectorSampler().run([qc]).result()
counts = result.get_counts()
$ qxlint example.pyexample.py:7:10: QXL101 get_counts() on a PrimitiveResult; counts live on the BitArray in PrimitiveResult -> PubResult -> .data (DataBin) -> <classical register> (BitArray)
Not flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler

qc = QuantumCircuit(2)
qc.measure_all()
result = StatevectorSampler().run([qc]).result()
counts = result[0].data.meas.get_counts()
$ qxlint example.pyNo findings

QXL102

A V1 result field read from a Primitives V2 result

  • default tier
  • error

Why it is a problem

quasi_dists is a field of the V1 SamplerResult and values is a field of the V1 EstimatorResult. A V2 PrimitiveResult has neither and raises AttributeError, verified on Qiskit 2.5.2 for both primitives. V2 exposes counts through the data bin as result[i].data.<register>.get_counts(), and expectation values as result[i].data.evs.

When it is legitimate

Reading quasi_dists from a V1 qiskit.primitives.SamplerResult, or values from a V1 EstimatorResult, is still valid, and this rule does not fire there. The same holds for a Runtime V1 result: qiskit_ibm_runtime.Sampler was SamplerV1 until 0.28, so on a target proven to predate that release its result is not a V2 one and nothing is reported. It also stays silent on any object whose type cannot be proven, so a helper that returns an unannotated result is never flagged on the strength of the attribute name alone.

Flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler

qc = QuantumCircuit(2)
qc.measure_all()
result = StatevectorSampler().run([qc]).result()
dists = result.quasi_dists
$ qxlint example.pyexample.py:7:9: QXL102 quasi_dists does not exist on a V2 PrimitiveResult; read result[i].data.<register>.get_counts() instead
Not flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler

qc = QuantumCircuit(2)
qc.measure_all()
result = StatevectorSampler().run([qc]).result()
counts = result[0].data.meas.get_counts()
$ qxlint example.pyNo findings

QXL103

Circuit with no measurement instructions passed to a SamplerV2

  • default tier
  • warning

Why it is a problem

A Sampler reports classical bits. A circuit with no measurement instruction produces nothing useful, and the failure is quiet. With no classical register, Qiskit 2.5.2 emits only a UserWarning and returns an empty data bin. With a classical register but no measure instruction, there is no warning at all and every shot reads as zeros, which looks like a physics result rather than a mistake.

When it is legitimate

Never for a Sampler, which is why the rule is limited to one. An unmeasured circuit is the correct and normal input to an Estimator, and this rule never looks at Estimators. It also requires proof: it fires only when the circuit was built and mutated in view, never reached unmodeled code, and has no measurement on any path.

Flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
StatevectorSampler().run([qc])
$ qxlint example.pyexample.py:7:1: QXL103 circuit has no measurement instructions but is passed to a SamplerV2; the result carries no counts
Not flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
StatevectorSampler().run([qc])
$ qxlint example.pyNo findings

QXL104

A circuit method that returns a new circuit is called and its result dropped

  • default tier
  • error

Why it is a problem

Several QuantumCircuit methods return a new circuit rather than mutating the receiver, and compose, tensor and assign_parameters default to inplace=False. Written as a bare statement the call does nothing at all, and nothing reports it: verified on Qiskit 2.5.2, qc.compose(other) as a statement leaves qc with zero instructions, and qc.assign_parameters({t: 1.0}) leaves the parameter unbound. The circuit then runs, and the result is wrong rather than missing.

When it is legitimate

When the method really does mutate, which is why the rule reads the inplace argument instead of the method name alone: qc.compose(other, inplace=True) and qc.measure_all() are both correct and neither is flagged. It also only fires on a bare expression statement, so assigning or returning the result is never flagged, and only on a receiver proven to be a circuit.

Flagged
from qiskit import QuantumCircuit

qc = QuantumCircuit(2)
other = QuantumCircuit(2)
other.h(0)
qc.compose(other)
$ qxlint example.pyexample.py:6:1: QXL104 this statement does nothing: compose() defaults to inplace=False and the result is discarded
Not flagged
from qiskit import QuantumCircuit

qc = QuantumCircuit(2)
other = QuantumCircuit(2)
other.h(0)
qc = qc.compose(other)
$ qxlint example.pyNo findings

QXL105

Circuit with measurements passed to a StatevectorEstimator

  • default tier
  • error

Why it is a problem

An Estimator computes expectation values from the statevector and has no use for classical bits. StatevectorEstimator refuses them outright: verified on Qiskit 2.5.2, it raises QiskitError('Cannot apply instruction with classical bits: measure'). This usually appears when a circuit written for a Sampler is reused for an Estimator without removing the measurements.

When it is legitimate

Never for StatevectorEstimator, which is the only implementation this rule looks at. Other estimators are excluded on purpose: what a Runtime EstimatorV2 does with a measured circuit is decided server side and cannot be established offline, so qxlint says nothing about it rather than guessing. BackendEstimatorV2 is excluded for the same reason.

Flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp

qc = QuantumCircuit(2)
qc.h(0)
qc.measure_all()
StatevectorEstimator().run([(qc, SparsePauliOp('ZZ'))])
$ qxlint example.pyexample.py:8:1: QXL105 circuit has measurements but is passed to a StatevectorEstimator, which raises on classical bits
Not flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp

qc = QuantumCircuit(2)
qc.h(0)
StatevectorEstimator().run([(qc, SparsePauliOp('ZZ'))])
$ qxlint example.pyNo findings

QXL201

channel="ibm_quantum" is removed in qiskit-ibm-runtime 0.41

  • default tier
  • error
  • version gated

Why it is a problem

The "ibm_quantum" channel was deprecated in qiskit-ibm-runtime 0.40.0 and removed in 0.41.0 with the sunset of IBM Quantum Platform Classic. Verified on qiskit-ibm-runtime 0.49.0: QiskitRuntimeService raises ValueError during construction, and QiskitRuntimeService.save_account raises InvalidAccountError, so both call sites are checked. Both "ibm_cloud" and "ibm_quantum_platform" point at the same API, "ibm_quantum_platform" is the default, and the argument can be omitted entirely.

When it is legitimate

On a target proven to predate qiskit-ibm-runtime 0.41 the value still works, and the rule downgrades to a deprecation notice from 0.40 and stays silent below it. A target that cannot be established is read as current, so the rule reports: a project that never states its version is far more likely to be running today's release than one from before 0.41, and staying silent there hid the finding from every project without a declared target.

Flagged
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(channel="ibm_quantum", token=TOKEN)
$ qxlint example.pyexample.py:3:40: QXL201 channel="ibm_quantum" was removed in qiskit-ibm-runtime 0.41; omit the channel argument
Not flagged
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(token=TOKEN)
$ qxlint example.pyNo findings

QXL202

Runtime SamplerV2 or EstimatorV2 given backend= or session= instead of mode=

  • default tier
  • error

Why it is a problem

The V1 Runtime primitives took backend= and session=. The V2 primitives replaced both with a single mode=, which accepts a backend, a Session or a Batch. Passing the old name raises TypeError immediately: verified on qiskit-ibm-runtime 0.49.0, SamplerV2(backend=...) fails with unexpected keyword argument 'backend'. This is one of the most common leftovers of a V1 to V2 migration, because the call still reads as if it should work.

When it is legitimate

Never on a V2 Runtime primitive. It is very much legitimate on Session and Batch, which really do take backend=, and this rule never looks at those. It also stays silent unless the constructor is proven to be a Runtime SamplerV2 or EstimatorV2, so a local class of the same name is not flagged. The bare names Sampler and Estimator were the V1 classes until qiskit-ibm-runtime 0.28, so on a target proven to predate that release they take backend= and session= correctly and are not flagged. With no target declared the current package is assumed, where both names are the V2 classes.

Flagged
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2

service = QiskitRuntimeService()
backend = service.least_busy()
sampler = SamplerV2(backend=backend)
$ qxlint example.pyexample.py:5:29: QXL202 SamplerV2 takes mode=, not backend=; the V2 primitives replaced both backend= and session= with mode=
Not flagged
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2

service = QiskitRuntimeService()
backend = service.least_busy()
sampler = SamplerV2(mode=backend)
$ qxlint example.pyNo findings

QXL203

service= passed to Session or Batch, which dropped the argument

  • default tier
  • error

Why it is a problem

Session and Batch took a service argument until qiskit-ibm-runtime 0.33.2 and dropped it in 0.34.0, read from the published wheels. Passing it now raises TypeError before anything reaches the service, so the whole script fails at that line. Every tutorial written before that release opens with this call.

When it is legitimate

On a target proven to predate 0.34.0 the argument is still accepted, and the rule stays silent there. It also only fires on a service keyword, so Session(backend=...) and Session(mode=...) are never flagged, and only on the two classes that changed, resolved through the import rather than by name, so a local class called Session is not touched.

Flagged
from qiskit_ibm_runtime import QiskitRuntimeService, Session

service = QiskitRuntimeService()
session = Session(service=service, backend="ibm_brisbane")
$ qxlint example.pyexample.py:4:27: QXL203 Session takes no service argument; it was removed in qiskit-ibm-runtime 0.34 and passing it raises TypeError
Not flagged
from qiskit_ibm_runtime import QiskitRuntimeService, Session

service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
session = Session(backend=backend)
$ qxlint example.pyNo findings

QXL204

A V2 primitive's run() called the way V1 took its arguments

  • default tier
  • error

Why it is a problem

The V1 primitives took parallel lists: estimator.run(circuits, observables, parameter_values). Every V2 primitive takes one argument, a list of pubs, plus a keyword only shots for a sampler or precision for an estimator. Verified on Qiskit 2.5.2 and qiskit-ibm-runtime 0.49.0: a second positional argument, any of the three V1 keywords, and shots on an estimator each raise TypeError. Changing the import without changing the call is the defining break of Primitives V2.

When it is legitimate

Never on a proven V2 primitive, because the call cannot execute. The rule reads the receiver's kind rather than the method name, so a V1 primitive keeps its own grammar and is not flagged, and so is any object whose type could not be proven. shots is only wrong on an estimator, and is left alone on a sampler where it is the documented keyword.

Flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp

qc = QuantumCircuit(2)
qc.h(0)
StatevectorEstimator().run([qc], [SparsePauliOp('ZZ')])
$ qxlint example.pyexample.py:7:1: QXL204 run() on an EstimatorV2 takes one argument, a list of pubs; the V1 form took parallel lists, so this call raises TypeError
Not flagged
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp

qc = QuantumCircuit(2)
qc.h(0)
StatevectorEstimator().run([(qc, SparsePauliOp('ZZ'))])
$ qxlint example.pyNo findings

QXL205

An import or a method call naming something Qiskit 1.0 or 2.0 removed

  • default tier
  • error

Why it is a problem

Qiskit 1.0 removed the top level execute, Aer and IBMQ, the opflow and algorithms packages, the vendored providers.aer, and QuantumInstance. Qiskit 2.0 removed assemble and the V1 primitives Sampler, Estimator, BackendSampler and BackendEstimator. Each release was read from the published wheels and each absence confirmed on Qiskit 2.5.2. The import raises before a single line of the script runs, so nothing downstream can be reached. Two QuantumCircuit methods went the same way, bind_parameters and qasm, and are reported on the call rather than on an import.

When it is legitimate

On a target proven to predate the release that removed the name, the import still works and the rule stays silent. It reads the dotted path of the import rather than the bound name, so a local module of the same name is not touched, and a relative import, which names nothing outside the package, is never considered. A removed method is reported only on a receiver the analyzer has proved to be a QuantumCircuit, so a method of the same name on any other object is left alone.

Flagged
from qiskit import QuantumCircuit, execute, Aer

qc = QuantumCircuit(2)
bound = qc.bind_parameters({})
$ qxlint example.pyexample.py:1:1: QXL205 qiskit.execute was removed in Qiskit 1.0; this import raises before anything else runsexample.py:1:1: QXL205 qiskit.Aer was removed in Qiskit 1.0; this import raises before anything else runsexample.py:4:9: QXL205 QuantumCircuit.bind_parameters was removed in Qiskit 1.0; calling it raises AttributeError
Not flagged
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator

qc = QuantumCircuit(2)
bound = qc.assign_parameters({})
$ qxlint example.pyNo findings

QXL300

Control flow nests deeper than the 12 levels qxlint walks

  • default tier
  • warning
  • library API

Why it is a problem

The circuit rules descend into control flow blocks to a fixed depth of 12, which bounds the work a circuit can ask for. Anything below that depth is never visited, so an unsupported operation there produces no finding. Without this rule the result is indistinguishable from a circuit that really is compatible, and a caller would read the empty list as an answer it is not.

When it is legitimate

Never a defect in the circuit itself: it reports a limit of qxlint, not a mistake in the code. It is still worth acting on, because the checks that ran cover only part of the circuit. Flattening the nesting, or checking the inner blocks as circuits of their own, gives a complete answer.

Flagged
# thirteen nested if_else blocks; the innermost is never visited
qxlint.check_target(deeply_nested, backend.target)

The rule page sketches this circuit rather than building it, so no output is shown.

Not flagged
# the inner block checked as a circuit in its own right
qxlint.check_target(inner_block, backend.target)

The rule page sketches this circuit rather than building it, so no output is shown.

QXL301

Circuit uses an operation or qubit the target does not support

  • default tier
  • error
  • library API

Why it is a problem

A backend accepts only the operations in its Target, on the qubit pairs its coupling map allows. Submitting anything else fails after the job is queued. This is a target compatibility check, not a hardware readiness check: readiness also involves layout, observable handling, option combinations and target freshness, none of which this rule looks at.

When it is legitimate

When the circuit is meant to be transpiled later. Running generate_preset_pass_manager(backend=...).run(qc) before submission is the normal fix, and an untranspiled circuit is not a defect on its own. This rule is only meaningful on a circuit you believe is already ISA compliant, which is why it is a library call rather than a file linting rule.

Flagged
qc = QuantumCircuit(2)
qc.cx(0, 1)
qxlint.check_target(qc, backend.target)  # cx is not native on Heron
>>> qxlint.check_target(qc, backend.target)circuit-41[0]: QXL301 operation 'cx' on qubits [0, 1] is not supported by the target
Not flagged
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
qxlint.check_target(pm.run(qc), backend.target)

The rule page sketches this circuit rather than building it, so no output is shown.

QXL302

Two adjacent identical self inverse gates cancel out

  • preview tier
  • note
  • library API

Why it is a problem

A gate that is exactly its own inverse, applied twice in a row to the same ordered qubits, is the identity. The allow list is explicit and was built by squaring each operator matrix and comparing with the identity, not by guessing from the class name: iSwap, S, T, SX and DCX look similar and are not self inverse, so they are absent. Qubit order matters, so cx(0, 1) followed by cx(1, 0) does not cancel.

When it is legitimate

Benchmarking, noise characterization, randomized benchmarking and calibration sequences deliberately apply canceling pairs to build a known identity circuit. Dynamical decoupling sequences do the same. This is why the rule is preview and informational rather than default on. It also only looks at immediately adjacent instructions, because allowing anything in between requires commutativity analysis.

Flagged
qc.h(0)
qc.h(0)

The rule page sketches this circuit rather than building it, so no output is shown.

Not flagged
qc.h(0)
qc.x(0)
qc.h(0)

The rule page sketches this circuit rather than building it, so no output is shown.

QXL303

A qubit is declared but never operated on

  • preview tier
  • note
  • library API

Why it is a problem

A qubit with no operation on it contributes nothing to the result but still widens the circuit, which can force a larger layout and a worse transpilation. Barriers, delays and identity gates do not count as use.

When it is legitimate

Often, which is why this is preview and informational. A transpiled circuit is skipped outright, since it carries a layout and its width is the device's rather than the author's. Ancillas reserved for a later step, fixed width protocol circuits, templates and circuits built to a register size all legitimately carry unused qubits, and those are still reported.

Flagged
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
>>> qxlint.check_circuit(qc, preview=True)circuit-41: QXL303 qubit 2 is declared but never operated on
Not flagged
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
>>> qxlint.check_circuit(qc, preview=True)No findings

How it decides

It asks what an object is.

get_counts() is right on a BitArray and wrong on a DataBin, so the question is never whether a call appears. qxlint answers it with a small abstract interpreter that follows objects through aliases, containers, Qiskit’s library circuits and the transpile pipeline.

Names point at objects, not at facts.

So an alias carries what was done through it.

qc = QuantumCircuit(1)
alias = qc
alias.measure_all()
sampler.run([qc])          # silent, the measurement is on the same object

A local container is not an escape.

This is how most Sampler code is written, so it has to be analyzable.

qc = QuantumCircuit(1)
circuits = []
circuits.append(qc)
sampler.run(circuits)      # QXL103 fires: the circuit is still tracked

Effects are scoped.

A call that cannot reach a circuit does not affect it; a call that receives it does.

qc = QuantumCircuit(2)
qc.h(0)
print("running")           # cannot touch qc
sampler.run([qc])          # QXL103 fires

qc2 = QuantumCircuit(2)
helper(qc2)                # may keep and mutate it
sampler.run([qc2])         # silent

Proof, or silence.

A rule fires only on what the analyzer can prove, never on maybe and never on unknown.

qc = QuantumCircuit(1)
if condition:
    qc.measure_all()
sampler.run([qc])          # QXL103 stays silent: measured on some paths

In every example sampler is a StatevectorSampler(). Each one is run through qxlint whenever this page is built, and the page is not published unless its comments still hold.

QXL201 reads the qiskit-ibm-runtime version your project declares
Declared targetWhat QXL201 reports
0.48, >=0.41An error: removed in 0.41
0.40.2, ==0.40.*A warning: deprecated since 0.40
>=0.38,<0.43An error, because the range does not prove the code is safe
Not declaredAn error, because an unstated target is read as current
0.39Nothing, because a pin below 0.40 predates the deprecation

The target comes from --target-runtime, then [tool.qxlint], then your project’s dependencies, an unambiguous uv.lock pin, or requirements.txt. Never from the Qiskit qxlint itself runs with.

Notebooks

Notebooks, cell by cell.

.ipynb files are read directly, and what qxlint knows carries across cells in order. Magics are not blanked out, because blanking lies to the analyzer: each is handled by what it can actually do.

KindExamplesHandling
Display or configuration%matplotlib, %pip, !cmdDropped, facts kept
Python body%%time, %%capture, %timeHeader dropped, body analyzed
Namespace changing%run, %load, %pylab, unknown magicsA barrier: facts before it are not trusted
Not Python%%bash, %%sql, %%htmlWhole cell dropped, a barrier

Every rewrite keeps the line count, so a reported line is the line you see in the cell.


Integrations

In your editor. In your gate.

The same analyzer behind every way in, so on the same version and configuration the editor and CI agree. Exit code 0 is clean, 1 is findings, and 2 means qxlint could not run.

  • pre-commit

    Two hooks, one for Python files and one for notebooks.

    repos:
      - repo: https://github.com/TuguiDragos/qxlint
        rev: v0.3.7
        hooks:
          - id: qxlint
          - id: qxlint-notebook
  • GitHub Actions

    SARIF for code scanning. The tag pins the analyzer too, so @v0.3.7 installs qxlint 0.3.7.

    - uses: TuguiDragos/qxlint@v0.3.7
      with:
        paths: .
        format: sarif
        output: qxlint.sarif
  • VS Code

    Diagnostics in .py files and in notebook cells, from the same CLI and the same configuration as CI.

    pip install qxlint
  • flake8

    A plugin for .py files. Use the CLI or nbqa for notebooks.

    pip install qxlint flake8
    flake8 --select=QXL .
  • A baseline

    Adopt it on a project that already has findings: record them once, then gate on what is added.

    qxlint --baseline-write qxlint-baseline.json
    qxlint --baseline qxlint-baseline.json
  • Configuration

    In pyproject.toml. Silence one line with # noqa: QXL101.

    [tool.qxlint]
    select = ["QXL1", "QXL2"]
    ignore = ["QXL102"]
    target-runtime = "0.48"

Tested

Run on code it had never seen.

244 public repositories, chosen and pinned to a commit before qxlint was run on any of them.

51,711 files

50,385 Python files and 1,326 notebooks, from 243 different owners.

0 failed runs

No repository made qxlint exit with code 2, which means it could not run.

6,283 findings

320 of them outside QXL205, the rule for names Qiskit removed.

407 read one by one

Each read in context and labeled. The labels are an AI reviewer’s, not yet confirmed by a person, and the write-up says so.

  • No interprocedural analysis. A circuit changed inside a helper is not tracked, and a circuit that arrives as a parameter or a return value carries no facts.
  • No published precision figure. The labels are not an independent measurement, and the release gate says exactly what is and is not claimed.
  • One interpreter’s view. qxlint parses with the Python running it, so run the same version locally and in CI.
  • Client side only. What qxlint says about IBM Runtime is about its client side validation, read from its source, never about server behavior.

Scanned with qxlint 0.3.5 on August 23, 2026. The release gate: what is claimed, and what is not


Questions.

What is qxlint?

qxlint is a free, open source linter for Python code that uses Qiskit. It finds the mistakes the move from the V1 to the V2 primitives introduced, such as reading counts off the wrong object or sampling a circuit that has no measurements, by reading your source. It never imports or runs it.

How do I run qxlint?

Run uvx qxlint . in your project without installing anything, or install it with uv tool install qxlint, pipx install qxlint, or pip install qxlint. It needs Python 3.11 to 3.14.

Does qxlint need Qiskit or a quantum computer?

No. The source linter needs no Qiskit at all, makes no network requests, and needs no quantum hardware. Only the checks on circuits in memory need Qiskit, installed with pip install 'qxlint[circuit]'.

Does qxlint work on Jupyter notebooks?

Yes. It reads .ipynb files directly and carries what it knows across cells in order. Each magic is handled by what it can do: display magics are dropped, cell magics with a Python body are analyzed, and magics that can rebind names stop the analysis from trusting what came before.

Will qxlint report something that is fine?

It tries hard not to. A rule fires only on facts the analyzer can prove, never on maybe and never on unknown, and every rule page says when the pattern it catches is legitimate. If that cannot be written, the rule does not ship.

How do I silence a finding?

Add # noqa: QXL101 to the line, or list the rule under ignore in [tool.qxlint] in pyproject.toml. On a project that already has findings, record them once with --baseline-write and gate on what is added with --baseline.

Can I use qxlint in CI and in my editor?

Yes: as a pre-commit hook, as a GitHub Action that writes SARIF for code scanning, as a flake8 plugin, and as a VS Code extension that runs the same CLI on the same [tool.qxlint] configuration, so the editor and CI agree as long as they run the same qxlint version and the extension’s own settings add nothing.

Is qxlint free?

Yes. qxlint is free and open source under the MIT License.

Get qxlint.

Free and open source, under the MIT License.

uvx qxlint .