Quantum Academy begins operations on September 15, 2026. Enrollment opens soon.
Skip to content

Quantum Computing

Four Quantum Frameworks, One Bell Pair: A Practical Comparison for Developers

Marin Ivezic17 min read

Most explanations of quantum programming start with the physics. Superposition, entanglement, the Bloch sphere, a page of bra-ket notation, and then, four sections later, some code. That order works for a physics course. It doesn’t work for a developer who wants to know which library to pip install on a Tuesday afternoon.

So we are going to start with the code. One small program – the smallest one that does something genuinely quantum – written four times, in Qiskit, Cirq, PennyLane and Q#. The program is the same in every case. What differs is what each framework thinks a quantum program is, and that difference is the whole basis for choosing between them.

This is a comparison of software development kits (SDKs): the libraries and languages you write in. It’s not a comparison of quantum hardware, and none of these frameworks locks you to a single machine as tightly as their branding suggests.

The Bell pair, four times

Here is the program. Take two qubits. A qubit is the quantum unit of information: where a classical bit holds 0 or 1, a qubit holds a weighted combination of both, called a superposition, until you measure it and force a single answer out.

Put the first qubit into an even superposition, then link the second to it so that whatever the first turns out to be, the second matches. Measure both, a thousand times.

A classical program that flipped two independent coins would give you four outcomes in roughly equal proportion: 00, 01, 10, 11. This program gives you two: 00 about half the time and 11 about half the time, and never 01 or 10. The two qubits are entangled, which is the technical way of saying their outcomes are correlated in a way no independent random process can reproduce. That correlated pair is called a Bell pair, and it’s the quantum equivalent of “hello, world” – small enough to run on a laptop simulator in under a second, and impossible to fake classically with two independent bits.

Four versions follow. Each does the same thing, and each makes a different assumption about what a quantum program is.

Qiskit

from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator

qc = QuantumCircuit(2, 2)
qc.h(0)              # superposition on qubit 0
qc.cx(0, 1)          # entangle qubit 1 with qubit 0
qc.measure([0, 1], [0, 1])

sim = AerSimulator()
result = sim.run(transpile(qc, sim), shots=1000).result()
print(result.get_counts())

You build an object, you mutate it by calling methods on it, you hand it to a backend. h is the Hadamard gate, which takes a qubit sitting definitely at 0 and puts it into an even superposition of 0 and 1. cx is the controlled-NOT: it flips the second qubit if and only if the first is 1, and applying it to a qubit that is both is what creates the correlation. shots=1000 means run the whole thing a thousand times, because a single run of a quantum program tells you almost nothing – you get one sample from a distribution, and the distribution is the answer.

The transpile call is worth pausing on. It rewrites your circuit into gates the target machine actually implements, and rearranges qubits to respect which ones can physically talk to each other. On a simulator this is nearly a no-op. On hardware it can double the length of your circuit, and that has consequences we will come back to.

Cirq

import cirq

q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit([
    cirq.H(q0),
    cirq.CNOT(q0, q1),
    cirq.measure(q0, q1, key='result'),
])

result = cirq.Simulator().run(circuit, repetitions=1000)
print(result.histogram(key='result'))

Almost the same length, noticeably different in attitude. You create qubit objects first, with identities and positions, and then build a circuit out of operations applied to them. In Qiskit a qubit is an index into a register. In Cirq it is a thing, and LineQubit.range(2) says these two sit next to each other on a line. Swap in cirq.GridQubit and you are describing a two-dimensional chip layout.

Cirq also groups your operations into moments – time slices, each containing gates that run simultaneously on different qubits. Print a Cirq circuit and the columns you see are those moments. When you care about how long a circuit takes in wall-clock terms on a real device, and therefore how much time the qubits have to decay before you measure them, having timing in the data model rather than inferred from a list is the difference between guessing and knowing.

PennyLane

import pennylane as qml

dev = qml.device("default.qubit", wires=2)

@qml.qnode(dev)
def bell():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.probs(wires=[0, 1])

print(bell())

This one is structurally different, and the difference is the point.

There is no circuit object here. There is a Python function decorated into a QNode – a quantum node, meaning a function whose body describes a circuit and whose return value is a number, or an array of numbers, that came out of running it. You call bell() the way you would call any function. The circuit construction, the device dispatch and the execution all happen inside the call, and when the device is given a shot count, so does the sampling. The device above has no shot count, so it returns exact probabilities rather than sampled ones.

That design choice buys one specific thing: the function is differentiable. If the circuit took parameters, PennyLane could give you the gradient of its output with respect to those parameters, and you could hand that gradient to an optimizer. That’s not a convenience feature bolted on. It’s the reason the framework exists, and we will return to what it makes possible.

Q#

import Std.Measurement.*;

operation Bell() : (Result, Result) {
    use (q0, q1) = (Qubit(), Qubit());
    H(q0);
    CNOT(q0, q1);
    let r0 = M(q0);
    let r1 = M(q1);
    ResetAll([q0, q1]);
    return (r0, r1);
}

The first thing to notice is that this isn’t Python. Q# is a separate language with its own compiler, invoked from a Python or C# host program, or run directly from a notebook or the command line. The snippet above targets the current standalone Q# syntax, with import and use. Older tutorials use open declarations and using blocks, and those target the legacy QDK, so they compile under that toolchain rather than under the standalone compiler.

The second thing is use. Qubits are allocated in a scope, and Q# tracks that scope. The compiler won’t let you copy a qubit into two variables, because the no-cloning theorem says the underlying physics cannot do that, and a language that lets you write physically impossible code is a language that lets you write undebuggable code. ResetAll returns the qubits to a known state before releasing them, which matters on a real machine where the next program inherits whatever you left behind.

The third thing is that M(q0) returns a value you can branch on, in the middle of the program, using ordinary if statements, and then apply more gates based on what you found. In the three Python frameworks, a measurement conventionally ends the quantum part; you get results back into Python and decide what to do next. Some of them now support mid-circuit measurement and conditional operations, but it’s an extension to the model. In Q# it is just control flow, because Q# was designed around algorithms that need it.

What the four versions actually disagree about

Four programs, four answers to the question “what is a quantum program?”

Qiskit says: a circuit is a data structure you build and submit. Construct, transpile, run, read counts. It maps cleanly onto how quantum computing is usually taught and how gate-model hardware actually works.

Cirq says: a circuit is a schedule on specific hardware. Qubits have identity and location, operations have timing, and the noise the device will inflict on you is something you can model explicitly rather than discover afterwards.

PennyLane says: a circuit is a differentiable function. Everything else follows from that – the decorator, the return of expectation values rather than raw counts, the interfaces to PyTorch, TensorFlow and JAX.

Q# says: a quantum program is a program. With types, scopes, control flow, unit tests and a compiler that catches your mistakes. The quantum parts are operations in a language, not calls into a library.

None of these is wrong. They are bets on which part of the problem will turn out to be hard.

Why the machine underneath changes the code

The frameworks diverge partly because the hardware they were built for is in an awkward stage, and the awkwardness leaks upward into how you write code.

Every quantum processor available today is what the field calls NISQ – Noisy Intermediate-Scale Quantum. On the largest processors announced publicly, the effective algorithmic qubit count runs far below the physical qubit count, there is no error correction, and there is a limited window before the quantum state degrades into noise. Practically, this means circuit depth is a budget. Every gate you add costs fidelity, and past some depth that depends on the device and the day, your results become indistinguishable from random.

Three consequences shape how NISQ-era code gets written.

Circuits stay shallow, so the classical computer does most of the work. The dominant pattern is the variational algorithm: a short parameterised circuit runs on the quantum device, produces a number, a classical optimizer adjusts the parameters, and the loop repeats. The quantum processor is a subroutine called thousands of times, not the main program. This is exactly the shape PennyLane was designed around.

The transpiler is not an implementation detail. Your abstract circuit assumes any qubit can interact with any other. Real chips have a fixed connectivity graph, and connecting distant qubits means inserting swap operations, which cost depth, which costs fidelity. A circuit that looks fine on paper can triple in length after mapping. This is why Cirq puts qubit geometry in the type system.

Errors get mitigated rather than corrected. Error correction encodes one reliable logical qubit across many noisy physical qubits, and the overhead is far beyond current hardware. What we have instead is mitigation: statistical post-processing that estimates what the answer would have been without noise. It works, within limits, and it requires characterising your device rather than treating it as a black box.

Fault tolerance is the state where error correction works well enough that logical qubits stay coherent indefinitely and you can run circuits of arbitrary depth. In that regime most of the above stops mattering. Circuits get long, hardware detail gets abstracted away, and algorithms with millions of operations become writable. Q#’s design – control flow, resource estimation, hardware-agnostic algorithms – is a bet on that regime arriving.

The honest position is that both regimes are worth writing code for, and which one you target should follow from what you are trying to learn, not from which vendor’s roadmap you find most persuasive.

Where each framework earns its keep

Qiskit

The broadest of the four, and the one with the most learning material attached. The package layout has changed considerably since the original Terra/Aer/Ignis/Aqua split – Ignis in particular has been retired and its functionality redistributed – so treat older tutorials with suspicion and check the version they were written against.

What Qiskit does well: circuit construction with a clean API, a fast simulator with configurable noise models, application libraries for chemistry, optimization and machine learning, and direct submission to IBM’s superconducting hardware from a few lines of Python. If you want to run something on a real quantum processor this week, this is the shortest path.

The trade-off is gravitational pull. Qiskit works best inside IBM’s stack, and the deepest features – pulse-level control, the most current hardware options – track IBM’s own machines. You can export to OpenQASM and run elsewhere, but you are then using Qiskit as a circuit compiler rather than a platform.

Choose it if: you are learning quantum computing generally, you want hardware access with minimal ceremony, or you need pre-built algorithm implementations to compare against.

Cirq

The most explicit about hardware. If your question is “what will this circuit actually do on a device with these error rates and this connectivity,” Cirq gives you the vocabulary to ask precisely.

Its noise modelling is the standout feature. You can attach depolarizing channels, amplitude damping and custom noise processes to a circuit, then simulate and see the degradation. For anyone studying how algorithms behave as devices get noisier, that is the core workflow, and Cirq treats it as a first-class activity rather than an add-on.

Cirq is deliberately thinner on high-level algorithms. It is a toolkit for building and manipulating circuits, and it expects you to bring the algorithm or reach for a companion library such as OpenFermion for quantum chemistry. That thinness is a feature for research and a cost for anyone who wanted a worked example to start from.

Choose it if: you are doing research where noise behaviour is the object of study, you need fine control over gate scheduling and qubit placement, or you are working with hardware whose native gate set does not match the textbook.

PennyLane

The specialist, and the specialisation is deep enough to justify learning a second framework even if you already know a first.

The differentiability is genuine, not cosmetic. Given a parameterised circuit, PennyLane computes gradients using the parameter-shift rule – evaluating the same circuit at shifted parameter values and combining the results into an exact derivative – or by other methods where those are available. Those gradients compose with PyTorch, TensorFlow and JAX, meaning a quantum circuit can sit inside a larger model as one layer among many, trained end to end by the same optimizer that trains the classical layers.

Here is what that looks like in practice, extending the Bell circuit into something trainable:

import pennylane as qml
from pennylane import numpy as np

dev = qml.device("default.qubit", wires=2)

@qml.qnode(dev)
def circuit(params):
    qml.RY(params[0], wires=0)
    qml.CNOT(wires=[0, 1])
    qml.RY(params[1], wires=1)
    return qml.expval(qml.PauliZ(1))

params = np.array([0.1, 0.2], requires_grad=True)
opt = qml.GradientDescentOptimizer(stepsize=0.4)

for _ in range(50):
    params = opt.step(lambda p: circuit(p), params)

print(params, circuit(params))

That loop drives the circuit’s expectation value – the average of the measured outcome over many shots, weighted by their probabilities – toward its minimum. Twenty lines, and it is structurally identical to a variational eigensolver used to find a molecule’s ground state energy, or to a quantum classifier trained on labelled data. Reproducing this in a framework without built-in gradients means deriving and coding the parameter-shift arithmetic yourself, per circuit.

PennyLane is also the most hardware-agnostic of the four by design. Its plugin system lets the same program run on different backends by changing the device string, which makes it a reasonable choice when you don’t yet know where your code will eventually execute.

Choose it if: your work involves optimizing circuit parameters, you come from a machine learning background and want the workflow to feel familiar, or you want backend portability without rewriting.

Q#

The outlier, and the one whose value is easiest to underestimate from a syntax comparison.

Q# gives you a compiler that understands quantum semantics. It enforces no-cloning, manages qubit lifetimes, and supports classical control flow interleaved with quantum operations as a native construct rather than an extension. For algorithms that need adaptive behaviour – phase estimation with feedback, error correction cycles that measure ancilla qubits and apply conditional corrections – code written in Q# reads as the algorithm rather than as orchestration around it.

Its resource estimation tooling lets you write an algorithm too large for any current machine, run the estimator, and get back the number of logical qubits, physical qubits and runtime it would need under a stated error-correction scheme. A logical qubit is an error-corrected abstraction built from many physical qubits; the estimator makes that ratio concrete for a specific algorithm. This is how you answer questions about future hardware requirements without waiting for the hardware, and no Python framework offers an equivalent as a first-class feature.

The Quantum Katas – a graded set of Q# exercises with automated checking – are the strongest structured learning resource attached to any of these frameworks, and they teach quantum algorithms as much as they teach the language.

The cost is real. Q# is another language, another toolchain, and it sits somewhat apart from the Python tooling where most quantum work currently happens. If your goal is to run a few circuits and see what happens, that is a steep entry price.

Choose it if: you are building algorithms with substantial classical control flow, you need resource estimates for fault-tolerant designs, you are teaching quantum algorithms and want a language that catches learner errors, or you work inside Azure.

A decision path

Framework comparisons tend to end in a table where every option wins a category. Here is a sequence of questions instead, in the order that resolves the choice fastest.

Are you learning quantum computing from scratch, with no specific application in mind? Qiskit. The material is abundant, the API is forgiving, and hardware access requires no negotiation. Learn a second framework later, once you know what you want it for.

Does your problem involve tuning circuit parameters against an objective? PennyLane, regardless of your answer to the previous question. Anything variational – chemistry ground states, combinatorial optimization, quantum machine learning – is what it was built for, and the gradient machinery is the difference between an afternoon and a fortnight.

Is noise or hardware topology the thing you are studying? Cirq. When the deviation between ideal and actual is the object of interest rather than an obstacle, you want it in the data model.

Are you writing algorithms for hardware that doesn’t exist yet? Q#. Resource estimation and expressive control flow are the tools for that job, and the other three do not offer equivalents.

Are you already committed to a cloud platform? That narrows things considerably. IBM’s stack points to Qiskit, Azure to Q#. Braket accepts several. Commitment is a legitimate input, but check it against the questions above before treating it as decisive.

Two observations that cut across all of it. First, these are not exclusive choices. Using Qiskit for hardware runs and PennyLane for anything with a training loop is a common and sensible arrangement. Second, the transferable part is the quantum reasoning – how gates compose, why measurement destroys the state you spent the circuit building, what interference actually buys you. Once you have that, picking up a second framework takes days.

What travels between frameworks

These frameworks are more interoperable than the four-way comparison suggests, and this matters for anyone worried about picking wrong.

OpenQASM is a text format for describing quantum circuits, supported as an import and export target by multiple frameworks. A circuit built in one tool can frequently be serialised and executed through another. It is not lossless across every feature, but for standard gate-model circuits it works.

QIR, the Quantum Intermediate Representation, aims at the same problem one layer down: a compiler-level representation, built on LLVM, that multiple source languages can target and multiple hardware backends can consume. If it succeeds, the choice of source language becomes closer to a preference than a commitment.

PennyLane’s plugin system approaches interoperability from the application side, letting one program dispatch to backends provided by other frameworks. And several conversion utilities exist for translating circuits directly between Cirq and Qiskit representations.

The practical upshot: a circuit is a reasonably portable artefact. What’s not portable is the surrounding infrastructure – your gradient computation, your noise model, your error mitigation pipeline, your resource estimates. Those are where each framework’s real investment sits, and those are what you are actually choosing between.

Starting properly

If you take one thing from the four code samples, make it this: run them. All four frameworks ship simulators that handle a Bell pair instantly on any laptop. Type the Qiskit version, change shots from 1000 to 10, and watch the statistics fall apart. Delete the qc.cx(0, 1) line and see the correlation vanish. Add a third qubit and a second CNOT and predict the output before you run it.

Twenty minutes of that shows superposition and measurement as counts on a screen rather than notation on a page, because you are watching probabilities behave rather than reading about them. The theory then lands on something you have already seen happen.

What that hands-on start won’t give you is the structure around it – which of these frameworks matters for the work you will actually be asked to do, how quantum development connects to the cryptographic migration work already under way in most organisations, and what “quantum-ready” means for an engineering team as opposed to an individual curious on a weekend.

That is where structured programs earn their place. Quantum Academy runs certification programs for developers and engineers that take these frameworks from a first Bell pair through to algorithm implementation, with the surrounding context that self-study tends to skip. For readers whose interest runs toward the security side, PostQuantum.com carries deeper technical analysis of quantum’s cryptographic implications, and QuantumCareers.com maps how these skills translate into roles.

The frameworks will keep changing. The reasoning underneath them – why a measurement costs you the state, why depth is a budget, why the answer is a distribution – will not.