Skip to main content
IBM Quantum Platform

Qiskit SDK 2.3 release notes


2.3.0

Prelude

Qiskit v2.3.0 is a new feature release of the Qiskit SDK.

This new version significantly expands the C API for transpilation, making more of the Target (QkTarget) available for inspection from C, and allowing the DAGCircuit (QkDag) to be created and manipulated. This makes it possible to write custom transpiler passes when compiling against the stand-alone libqiskit object. There are individual transpilation-stage functions in the C API (for example, qk_transpile_stage_layout()), to make it easier to insert custom logic into transpilation without having to recreate the entire pipeline manually.

The performance and feature set of transpilation to early fault-tolerant targets has also been improved. With the new PauliProductMeasurement instruction, which represents a projective measurement into a Pauli product basis, Qiskit now fully supports transpiling circuits to a Pauli-based computation basis using the existing LitinskiTransformation pass. The new unitary synthesis method, RossSelingerSynthesis, which can be set in the transpiler using unitary_synthesis_method="gridsynth", enables using the asymptotically optimal Ross-Selinger algorithm for single-qubit Clifford+T synthesis. Other improvements include, for example, a better OptimizeCliffordT pass or the CommutativeOptimization pass, which takes into account commutative optimizations of Pauli-based circuits.

Internally, the representation of ControlFlowOp objects has changed to be Rust-native. This transformation is not yet complete; you may see some performance regression in Qiskit 2.3 around control-flow operations. Later versions of Qiskit should take this further, solving long-standing performance and API concerns around control-flow operations.

Note that from this release onwards, Python 3.9 is no longer supported due to having passed its end of life in October 2025, and macOS on Intel processors has been downgraded to tier 2 platform support due to Apple beginning to sunset the platform. All versions of CPython from 3.10 onwards are supported. See Operating system support for the current support tiers of different platforms.

There will be further feature releases of Qiskit in the 2.x series; we do not expect to release Qiskit 3.0 until much later in 2026, as there is currently no need for breaking changes.

C API Features

Circuits Features

  • Added a new instruction class, PauliProductMeasurement, which represents a joint projective measurement on multiple qubits, where the measured observable is a tensor product of Pauli operators. The outcome of this measurement is a single eigenvalue, either +1+1 or 1-1, indicating the eigenstate of the Pauli product.

    For additional background, see A Game of Surface Codes: Large-Scale Quantum Computing with Lattice Surgery by Daniel Litinski.

    A PauliProductMeasurement can be instantiated from a Pauli, where the Pauli may include a phase of 1-1, but not of ii or i-i. The instruction has the same number of qubits as the Pauli, and a single classical bit.

    As an example:

    from qiskit.circuit import QuantumCircuit
    from qiskit.quantum_info import Pauli
    from qiskit.circuit.library import PauliProductMeasurement
     
    ppm = PauliProductMeasurement(Pauli("XZ"))
     
    qc = QuantumCircuit(6, 2)
    qc.append(ppm, [4, 1], [1])
  • A new QuantumCircuit.to_dag() method now provides a convenient wrapper around circuit_to_dag().

  • ParameterExpression now has a num_parameters attribute, which is equal to the length of its parameters set, but calculated with less overhead.

OpenQASM Features

  • The OpenQASM 2 exporter (qasm2.dumps()) now supports outputting simple single-instruction IfElseOp blocks, if the condition is a simple register–integer equality test. This corresponds to what the OpenQASM 2 language can represent.

QPY Features

  • Version 17 of QPY now includes serialization formats for SparseObservable objects when used as parameters of objects. In particular, this allows to serialize PauliEvolutionGate objects that internally use this operator and the payload of the evolution gate is updated.

Quantum Information Features

  • Added Statevector.from_circuit() as a mirror of Operator.from_circuit(). This allows directly instantiating a Statevector in the space of virtual qubits, even for circuits that have been transpiled to a physical-qubit space.

  • Improved the performance of Clifford.dot() and of Clifford.compose() when called with front=True and a QuantumCircuit containing only Clifford gates. Previously, the circuit was completely consumed into a Clifford object first, and then the two objects were combined. Now, the base Clifford is updated iteratively, which is significantly faster for small circuits.

  • Added the PauliLindbladMap.parity_sample() method. This method is very similar to the pre-existing PauliLindbladMap.signed_sample() method, however it uses a sign convention that is more consistent with the rest of Qiskit, and therefore is the preferred method to use going forward. It has additional optional arguments to apply scalings to the rates when sampling without modifying the instance.

  • Several of the quantum_info “predicates” functions, such as is_identity_matrix(), were optimized to avoid unnecessary matrix allocations.

Synthesis Features

Transpiler Features

  • Added a new transpiler pass, CommutativeOptimization, which performs gate cancellation and merging, exploiting commutativity relations. The pass unifies and extends the functionality of both CommutativeCancellation and CommutativeInverseCancellation.

    Specifically, the pass:

    • Cancels pairs of inverse gates, including pairs that are inverse up to a global phase (adjusting the global phase if necessary).
    • Attempts to merge consecutive gates when possible, for example sequences of RZ-gates, RX-gates, Pauli rotations, and so on.
  • Added a new option, fallback_on_default, to the UnitarySynthesis transpiler pass. This option applies when the pass is called with a non-default synthesis plugin, specified via the argument method.

    By default, the specified plugin is used to synthesize every unitary in the circuit (provided the plugin applies; for instance, it will not run if it does not support the number of qubits over which the unitary is defined). If the plugin cannot synthesize the unitary and returns None, the original unitary is left unchanged in the circuit. When fallback_on_default is set to True, the pass will instead invoke DefaultUnitarySynthesis plugin when the specified method fails.

    This feature is particularly useful when custom plugins are intended to handle only a subset of all unitaries: users can rely on their custom logic when it applies and use the default synthesis where it does not.

  • Added a new unitary synthesis plugin, RossSelingerSynthesis, which synthesizes single-qubit unitary gates using the Ross-Selinger algorithm and produces a single-qubit quantum circuit consisting of Clifford, TT and TT^\dagger gates.

    The plugin is invoked by the UnitarySynthesis transpiler pass when the parameter method is set to "gridsynth".

  • When transpiling to the Clifford+T basis, the UnitarySynthesis transpiler pass now uses a Clifford+T synthesis algorithm by default to approximate single-qubit unitaries.

    Similarly, the DefaultUnitarySynthesis plugin now uses the same Clifford+T synthesis algorithm to approximate single-qubit unitaries.

  • A new DAGCircuit.to_circuit() method now provides a convenient wrapper around dag_to_circuit().

  • Added a new transpiler pass SubstitutePi4Rotations, that converts single-qubit RZGate, RXGate and RYGate rotation gates whose angles are integer multiples of π/4\pi/4 into discrete sets of Clifford, TGate and TdgGate gates. Note that odd multiples of π/4\pi/4 require a single TGate and TdgGate, as well as some Clifford gates, while even multiples of π/4\pi/4, or equivalently, integer multiples of π/2\pi/2, can be written using only Clifford gates.

  • The transpiler pass LitinskiTransformation has been extended to handle measurements. Thus, the transform now applies to a circuit containing Clifford, single-qubit RZ-rotation gates (including TT and TT^\dagger), and standard Z-measurements, and moves Clifford gates to the end of the circuit. In the process, it changes RZ-rotations to product Pauli rotations (implemented as PauliEvolutionGate gates), and changes Z-measurements to product Pauli measurements (implemented using PauliProductMeasurement instructions).

  • The OptimizeCliffordT transpiler optimization pass has been significantly enhanced and reimplemented in Rust.

    This pass performs a peephole optimization on circuits expressed using the Clifford+T gateset. More precisely, it collapses all chains of single-qubit gates containing Clifford+T into a minimal usage of TT (or TT^\dagger). The pass runs on a chain in time proportional to the number of gates in the chain.

  • Added WrapAngles.DEFAULT_REGISTRY containing the default registry for WrapAngles. This new attribute supersedes the previous WRAP_ANGLE_REGISTRY, and we encourage downstream users to change to the new form as soon as possible.

  • The DAGCircuit.topological_op_nodes() and DAGCircuit.topological_nodes() methods now support a reverse Boolean argument. When set to True, the methods yield nodes in a reverse topological order, from the outputs of the circuit towards the inputs. This provides a direct and efficient way to iterate over a DAG backwards without the overhead of explicitly reversing the list of nodes returned by the functions, creating a new, structurally reversed DAG with DAGCircuit.reverse_ops().

  • The CommutationChecker now supports efficient commutation checks between the Pauli-based gates PauliGate, PauliEvolutionGate and PauliProductMeasurement by checking if the generating Pauli operators commute. This enables optimizations, in particular for circuits in Pauli-based computation format, expressed in terms of Pauli evolutions and Pauli product measurements. Note that commutations between these Pauli-based gates and other standard gates are not yet handled in the same efficient manner.

  • Added a new argument, matrix_max_num_qubits, to CommutationChecker.commute(). This allows to limit the size of instructions for which the commutation checker is allowed to compute the exponentially expensive matrix representation. This new argument allows to distinguish between the size limit on instructions to handle (set via max_num_qubits which is newly None per default, meaning no limit) and a matrix size limit.

  • The functions generate_preset_pass_manager() and transpile() now take the argument unitary_synthesis_method into account when compiling into Clifford+T basis set, thus allowing the invocation of custom unitary synthesis plugins.

  • VF2Layout and VF2PostLayout now track partial candidate-layout scores during subgraph isomorphism matching. This has no meaningful effect when there is no perfect layout to find, but can drastically reduce the cost of scoring layouts to choose the best candidate in high-symmetry cases.

  • The call_limit argument of VF2Layout and VF2PostLayout can now be a 2-tuple, where the first item is used before the first match is found, then the limit swaps to the second after. This is a more reliable runtime-limiter than the max_trials argument, now that layouts are scored on the fly with more aggressive pruning.

Miscellaneous Features

Upgrade Notes

  • ConsolidateBlocks now reads a PropertySet key ConsolidateBlocks_qubit_map on entry. This key and its value are not public and should not be read or written to by other passes.

C API Upgrade Notes

  • qk_target_entry_new_fixed() has an additional name parameter, to set the name of the target entry. Fixed-angle entries almost invariably require overriding the name of the standard gate to function correctly.

Circuits Upgrade Notes

  • The names of the circuits produced as definitions of standard circuit library gates are now set to None. Previously, these circuits had the same name as the gate, making it very easy to construct opaque gates with names matching a standard gate. This violates a common assumption in Qiskit, where standard gate names should be unique.

  • The internal representation of ControlFlowOps has changed when they are added to a QuantumCircuit or DAGCircuit. The object stored in the circuit and returned on future access will not necessarily be the same instance added to the circuit. Users should not attempt to mutate any objects in-place once they have been added to a circuit. This is likely to corrupt the circuit, whether it is control flow or another object.

    As with all in-place mutations to Python objects stored within a QuantumCircuit or DAGCircuit, you must re-assign the instruction to the circuit in order for Rust space to pick up the modifications. For example, when adding annotations to a BoxOp that is already on a circuit, a transpiler pass should make sure to use DAGCircuit.substitute_node() to update the Rust-space object:

    from qiskit.circuit import QuantumCircuit, Annotation
     
    class MyAnnotation(Annotation):
        namespace = "my"
     
    qc = QuantumCircuit(2)
    with qc.box():
        qc.cx(0, 1)
    dag = qc.to_dag()
     
    # Modifications to the box's annotations in-place require
    # writing back the information to Rust space.
    box_node = next(dag.topological_op_nodes())
    box_node.op.annotations.append(MyAnnotation())
     
    # Write back the operation.
    dag.substitute_node(box_node, box_node.op)
  • Transpiler performance in the presence of ControlFlowOp instructions, including BoxOp, is expected to be temporarily worse in Qiskit 2.3, as we transition the internal representation of control flow from its previously Python-centric version to a Rust-native one. We expect the performance to improve again in a later version of Qiskit, and to enable us to resolve long-standing API deficiencies in transpiler passes acting on control-flow operations.

  • The blocks of ControlFlowOp instances will no longer track name or metadata fields. These were already unsettable with the control-flow builder interface, and their existence was an unintended implementation detail rather than an intentional API.

  • The method Gate.control() no longer returns an AnnotatedOperation when the argument annotated is set to True and a native controlled-gate class is available. This change is consistent with how the argument annotated is used across the standard circuit library, and enables more efficient decompositions of control-annotated gates. The affected gates are:

    • Single-controlled H, S, Sdg, U3, Y, Z, SX, RX, RY, RZ, Swap and CZ gates;
    • Double-controlled Z-gate;
    • arbitrarily-controlled Phase, CPhase, MCPhase, U1, CU1, MCU1 and MCMT gates.
  • The default value of the argument annotated in QuantumCircuit.control() is now None instead of False. This does not affect the actual circuits, but is consistent with the default value of annotated used across the circuit library.

QPY Upgrade Notes

  • The default QPY version (QPY_VERSION) used in qpy.dump() is now 17.

Quantum Information Upgrade Notes

  • Improved the performance of Statevector.expectation_value() for all-identity Pauli operators by relying on the optimized general-case implementation instead of a dedicated shortcut that scaled poorly with the number of qubits.

Transpiler Upgrade Notes

  • The maximum call and trial limits for the exact-matching run of VF2PostLayout at optimization_level=3 have been reduced to avoid excessive runtimes for highly symmetric trial circuits being mapped to large coupling maps.

  • The preferred location for the default registry for the WrapAngles transpiler pass is now WrapAngles.DEFAULT_REGISTRY. The previous deeply nested WRAP_ANGLE_REGISTRY path will continue to work for backwards compatibility, but we encourage downstream packages to use the new location. The previous path was an oversight in Qiskit 2.2, and traverses modules that were never intended to be part of the public API.

  • While the interface to VF2Layout and VF2PostLayout remain logically the same, the max_trials argument now has much less effect as a runtime limiter, other than when set to the value 1. This is because, with the new on-the-fly score-and-prune algorithm used internally by the class, “complete” layouts are encountered far more rarely, and the count of “trials” only increments when a new layout is encountered that is better in error rate than a previous one. You should instead use call_limit as the deterministic runtime limiter; where max_trials measures complete layouts encountered, call_limit measures partial-layout extensions.

Miscellaneous Upgrade Notes

  • The minimum supported version of Python is now 3.10, following the end of life of Python 3.9 in October 2025 and deprecation warnings in Qiskit since version 2.1.

  • Support for macOS x86-64 (Intel) has been downgraded from tier 1 to tier 2. Qiskit will still provide tested and pre-compiled wheels for this platform, but tests are only performed at the time of release, rather than at every change. This might cause delays in releasing the wheels for this platform.

    This change was made because Apple has begun to sunset the platform, and the Qiskit team does not have the developer or CI resources left to continue tier 1 support. We can only support macOS x86-64 while GitHub continues to provide runners for it, and we expect this support to be removed in the second half of 2027.

C API Deprecations

  • The function qk_transpiler_pass_standalone_vf2_layout() is deprecated, as callers should now use qk_transpiler_pass_standalone_vf2_layout_average(). The new function name is more descriptive of the scoring heuristic, and the API allows encapsulated access to the whole new configuration object, including the two-limit form of call_limit.

    This deprecation is not entirely necessary for users, but with the C API still explicitly unstable, we are using this as a trial of managing deprecations and compiler-specific warnings in the C API, before we reach stability guarantees.

Circuits Deprecations

  • Since Qiskit 1.0, the methods Gate.control() and QuantumCircuit.control() accept the argument annotated which can be either False, True or None. Presently, a controlled gate is represented using a dedicated controlled-gate class when it exists, regardless of the value of annotated, for instance a two-controlled version of an XGate is a CCXGate. If a dedicated controlled-gate class does not exist, the controlled gate is represented as a ControlledGate when annotated=False and as an AnnotatedOperation when annotated=True. The default value annotated=None is treated exactly the same as False.

    In Qiskit 3.0, we will no longer allow setting annotated=None and instead change set the default to annotated=True. This is recommended, as it defers the construction of the controlled circuit from the circuit construction to the transpiler, and enables additional controlled-gate optimizations, typically leading to higher-quality circuits (especially for hierarchical circuits).

    However, you will still be able to explicitly set annotated=False to preserve the previous behavior.

Transpiler Deprecations

Build System Changes

  • When building or packaging Qiskit from source, the version of setuptools required is now at least version 77.0 (released in March 2025). This is to support the new license-metadata specifications of PEP 639. This dependency is specified in the build requirements, and so no manual action should be needed.

Bug Fixes

  • Fixes the implementation of __deepcopy__() in QuantumCircuit which did not deep-copy circuit parameters. As a consequence, mutating a BoxOp in a copied circuit no longer affects the original circuit.

  • DAGCircuit.apply_operation_back(), apply_operation_back() and circuit_to_dag() will now add new edges in a deterministic order. The previous behavior could cause certain transpiler passes (such as SabreSwap) to traverse the DAG in non-deterministic orders.

  • DAGCircuit.apply_operation_front() can no longer insert invalid self loops when handling nodes that include classical conditions.

  • Fixed an issue in the Optimize1qGatesDecomposition when the pass was initialized with a Target that contains 1q gates with fixed angle parameters. Previously, the pass would potentially output gates outside the target as it wasn’t checking that the gate in the target supported arbitrary parameter values. Fixed #14743.

  • Fixed incorrect behavior in the BasisTranslator pass where a multi-qubit gate within a ControlFlowOp block would track with its local qubit indices instead of using the absolute indices from the source circuit.

  • Fixed re-use of the same ConsolidateBlocks instance on multiple circuits, including calls to transpile() with more than one circuit and no process-based parallelization. A bug introduced in Qiskit 2.2.2 caused the pass to panic or produce invalid output if the same instance was re-used on differing circuits.

  • The ConsolidateBlocks transpiler pass will now correctly evaluate whether a given gate is hardware-supported while recursing into control-flow operations.

  • qpy.dump() can now handle writing out to .gz files opened using the standard-library gzip module with QPY versions 16 or greater. See #15157 for details.

  • Fixed the methods MCPhaseGate.inverse() and MCU1Gate.inverse() to preserve the control states of open-controlled gates when computing their inverses.

  • ConsolidateBlocks will now return a Python-space exception instead of panicking when it detects invalid or out-of-date analysis in the legacy run_list or block_list PropertySet keys.

  • Optimize1qGatesDecomposition will now raise a TranspilerError instead of a Rust-space panic when attempting to run on a circuit that is too large for the Target.

  • Fixed an issue with pickle support for the SabreSwap where a SabreSwap instance would error when being pickled after the SabreSwap.run() method was run. Fixed #15071.

  • The scheduling passes, ALAPScheduleAnalysis and ASAPScheduleAnalysis, will now correctly handle circuits with no operations in them. Previously they would raise a TranspilerError falsely claiming “No durations provided”. Fixed #15145.

  • Fixed an issue where is_unitary() was not properly respecting the input tolerance values when checking if an operator is unitary. The method now correctly uses the provided atol and rtol parameters when simplifying the operator and checking if it equals the identity. This fixes #14107.

  • Fixed a bug in UnitarySynthesis transpiler pass, when it is called with a non-default synthesis plugin (specified via method) that supports basis_gates but not target. The pass now correctly passes the basis gates from the target to the plugin.

  • Fixed a failure in the circuit text drawer, which could occur when circuit blocks inside control flow operations were defined on different registers than the outer circuit. This situation could for example happen when appending ControlFlowOp operations directly, or for circuits after transpilation.

  • Fixed support for serializing PauliEvolutionGate with operators of type SparseObservable, when using QPY version 17.

  • Fixed a mismatch in QkTargetOp in which the length of the array stored in QkTargetOp.params did not match the number exposed by QkTargetOp.num_params. Wildcard parameters are now represented by NAN, and the length of the array will always be QkTargetOp.num_params. This bug was only present in 2.3.0rc1.

  • Fixed a regression in the classical expression representation of Value of type Uint of width greater than 64 bits. Previously, the value of expressions with these types was unintentionally coerced into a double floating point value internally, resulting in loss of precision.

  • The HighLevelSynthesis pass will now correctly select the optimization metric according to the basis set: two-qubit gate count for continuous bases and T count for Clifford+T bases. Previously, this information was not correctly propagated, resulting in worse than expected T counts.

  • Fixed the circuit text drawer so that circuit instructions with classical wires are drawn in separate layers.

  • Matrix multiplication (@) between Operator and Statevector will now apply the operator to the state and return the resulting Statevector.

  • Fixed a bug in the ElidePermutations transpiler pass that caused it to crash when handling single-qubit permutations.

  • Fixed a potential deadlock issue when running layout passes such as SabreLayout with a disjoint connectivity in the Target and in a multiprocessing context from running PassManager.run() or transpile() with more than one circuit. On Linux this is the default behavior when running PassManager.run() or transpile() with more than one circuit; on all other platforms, you must opt in to using a multiprocessing context.

    This was due to an underlying issue in CPython tracked in python/cpython#84559 when mixing multiprocessing and multithreading. Typically Qiskit guards against mixing the two methods of parallelism, but in the case of handling disjoint connectivity graphs this guard was missing around multithreaded Rust code.

  • The method Statevector.to_dict() will now respect its decimals keyword argument, which it previously ignored.

  • Fixed an issue with the timeline_drawer() visualization function where it would error when visualizing a scheduled circuit from a target that had parameterized gates in the target with a duration set.

  • Fixed label generation for PauliEvolutionGate with SparseObservable operators. Labels now display Pauli operators with qubit indices (e.g., "X0 X2") instead of the concatenated string format (e.g., "XX"), improving clarity when distinguishing between different operators.

  • Pauli.evolve() now correctly handles quantum circuits containing certain parametrized rotation gates, when the angle is a multiple of π/2\pi/2, e.g. RZGate(math.pi/2) or RZZGate(math.pi/2). Formerly they were not recognized as Clifford gates, and an error was raised.

  • BasePassManager no longer replaces falsy but valid outputs from passes with the original input program. A pass returning 0, False or another falsy value will now be preserved, and only None indicates failure.

  • QuantumCircuit.compose() will now correctly remap any variables and stretches used in Delay instructions when the var_remap argument is specified.

Was this page helpful?
Report a bug, typo, or request content on GitHub.