PennyLane is a Python library for differentiable quantum computing. Its core idea is that quantum circuits with tunable parameters are differentiable functions, and you can compute their gradients the same way you compute gradients in machine learning. This enables a class of algorithms called variational quantum algorithms (VQAs), which are the most practically relevant algorithms for near-term quantum hardware.
This tutorial covers PennyLane's core architecture (QNodes, devices, and measurement), how gradient computation works, and builds a complete VQE implementation.
Installation
pip install pennylaneFor faster simulation:
pip install pennylane-lightninglightning.qubit uses a C++ backend and is roughly 10 to 100 times faster than default.qubit for circuits with more than about 8 qubits.
QNodes: The Core Abstraction
In PennyLane, a quantum circuit is a Python function decorated with @qml.qnode(device). The decorator binds the function to a backend device (simulator or hardware) and enables automatic differentiation.
import pennylane as qml
import numpy as np
# Create a device: 2-qubit statevector simulator
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def bell_state_circuit():
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
return qml.probs(wires=[0, 1])
probs = bell_state_circuit()
print(probs)
# [0.5, 0.0, 0.0, 0.5] (|00> and |11> each 50%)Calling bell_state_circuit() executes the circuit on the bound device and returns the measurement result. The device handles all state evolution internally.
Parameterized Circuits
The power of PennyLane comes from parameterized circuits. A parameterized circuit takes numerical parameters as inputs and uses them as gate rotation angles:
dev = qml.device("default.qubit", wires=1)
@qml.qnode(dev)
def rotation_circuit(theta, phi):
qml.RX(theta, wires=0) # Rotate around X-axis by theta radians
qml.RZ(phi, wires=0) # Rotate around Z-axis by phi radians
return qml.expval(qml.PauliZ(0))
# Expectation value of Z for theta=pi/4, phi=pi/3
result = rotation_circuit(np.pi / 4, np.pi / 3)
print(result)
# 0.7071... (cos(pi/4))qml.expval(qml.PauliZ(0)) returns the expectation value of the Z operator, which equals +1 for state |0⟩, -1 for state |1⟩, and intermediate values for superpositions.
The Parameter-Shift Rule
This is where PennyLane diverges from classical ML frameworks. You cannot use standard backpropagation through quantum hardware. The parameter-shift rule is an analytic gradient method for quantum gates.
For a gate G(theta) = exp(-i*theta*P/2) where P is a Pauli operator, the gradient of any expectation value F with respect to theta is:
dF/d(theta) = [F(theta + pi/2) - F(theta - pi/2)] / 2This requires two circuit evaluations per parameter, but gives the exact gradient (not an approximation). PennyLane computes this automatically when you call qml.grad:
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(params):
qml.RY(params[0], wires=0)
qml.RY(params[1], wires=1)
qml.CNOT(wires=[0, 1])
qml.RY(params[2], wires=0)
return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
params = np.array([0.1, 0.2, 0.3], requires_grad=True)
grad = qml.grad(circuit)
gradients = grad(params)
print(gradients)
# Gradient of the expectation value with respect to each parameterFor default.qubit and lightning.qubit, PennyLane uses backpropagation (faster). For hardware devices, it uses the parameter-shift rule automatically.
Measurement Types
PennyLane supports several measurement types inside QNodes:
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def measurements_example(theta):
qml.RY(theta, wires=0)
qml.CNOT(wires=[0, 1])
return (
qml.expval(qml.PauliZ(0)), # Expectation value: scalar float
qml.probs(wires=[0, 1]), # Probability distribution: 4-element array
qml.var(qml.PauliX(0)), # Variance: scalar float
)
expval, probs, var = measurements_example(np.pi / 3)
print(f"<Z>={expval:.4f}, var(X)={var:.4f}")
print(f"Probs: {probs}")For optimization, qml.expval is the standard choice. It returns a differentiable scalar that can be minimized by gradient descent.
VQE: A Complete Implementation
VQE (Variational Quantum Eigensolver) finds the ground state energy of a Hamiltonian by minimizing the expectation value ⟨ψ(θ)|H|ψ(θ)⟩ over circuit parameters θ. This is the primary near-term algorithm candidate for quantum chemistry simulation.
Here is a complete VQE implementation for a simplified 4-qubit system using a hardware-efficient ansatz:
import pennylane as qml
import numpy as np
dev = qml.device("lightning.qubit", wires=4)
def ansatz(params, wires):
"""
Hardware-efficient ansatz: alternating single-qubit rotations and CNOTs.
params shape: (n_layers, n_qubits, 3) for Rot angles per qubit per layer
"""
n_qubits = len(wires)
for layer_params in params:
# Single-qubit rotation layer
for i, w in enumerate(wires):
qml.Rot(
layer_params[i][0],
layer_params[i][1],
layer_params[i][2],
wires=w
)
# Nearest-neighbor entangling layer
for i in range(n_qubits - 1):
qml.CNOT(wires=[wires[i], wires[i + 1]])
# Define Hamiltonian (simplified 4-qubit Ising-like Hamiltonian)
coeffs = [-1.0, 0.5, 0.5, 0.5, -0.25]
obs = [
qml.PauliZ(0) @ qml.PauliZ(1),
qml.PauliX(0),
qml.PauliX(1),
qml.PauliX(2),
qml.PauliZ(2) @ qml.PauliZ(3),
]
H = qml.Hamiltonian(coeffs, obs)
@qml.qnode(dev)
def vqe_circuit(params):
ansatz(params, wires=[0, 1, 2, 3])
return qml.expval(H)
# Initialize parameters randomly
n_layers = 2
n_qubits = 4
np.random.seed(42)
params = np.random.uniform(-np.pi, np.pi, (n_layers, n_qubits, 3))
# Optimize with Adam
opt = qml.AdamOptimizer(stepsize=0.1)
energies = []
for step in range(100):
params, energy = opt.step_and_cost(vqe_circuit, params)
energies.append(float(energy))
if step % 20 == 0:
print(f"Step {step:3d}: energy = {energy:.6f}")
print(f"\nFinal energy: {energies[-1]:.6f}")Example output:
Step 0: energy = -0.312847
Step 20: energy = -1.124503
Step 40: energy = -1.489021
Step 60: energy = -1.621334
Step 80: energy = -1.648920
Final energy: -1.651200The energy converges toward the Hamiltonian's ground state eigenvalue. On real quantum hardware, gate noise and decoherence add variance to the energy estimates, making convergence slower and less precise than this simulation shows.
QAOA: Quantum Approximate Optimization
QAOA is PennyLane's second major variational algorithm. It approximates solutions to combinatorial optimization problems by alternating problem and mixer Hamiltonians.
Here is a MaxCut QAOA implementation for a small graph:
import pennylane as qml
import numpy as np
# Graph edges for MaxCut
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]
n_qubits = 4
p = 2 # QAOA depth (number of layers)
dev = qml.device("default.qubit", wires=n_qubits)
def qaoa_layer(gamma, beta):
# Problem unitary: ZZ interactions per edge
for i, j in edges:
qml.ZZPhaseShift(2 * gamma, wires=[i, j])
# Mixer unitary: X rotations
for w in range(n_qubits):
qml.RX(2 * beta, wires=w)
@qml.qnode(dev)
def qaoa_circuit(gammas, betas):
# Initial state: uniform superposition
for w in range(n_qubits):
qml.Hadamard(wires=w)
# QAOA layers
for gamma, beta in zip(gammas, betas):
qaoa_layer(gamma, beta)
# Cost: sum over edges of (1 - Z_i Z_j) / 2
cost_terms = [
0.5 * (qml.Identity(0) - qml.PauliZ(i) @ qml.PauliZ(j))
for i, j in edges
]
return qml.expval(qml.sum(*cost_terms))
gammas = np.array([0.5, 0.8], requires_grad=True)
betas = np.array([0.3, 0.6], requires_grad=True)
# Optimize
opt = qml.GradientDescentOptimizer(stepsize=0.1)
for step in range(50):
(gammas, betas), cost = opt.step_and_cost(
lambda g, b: qaoa_circuit(g, b), gammas, betas
)
print(f"Optimized expected cut value: {qaoa_circuit(gammas, betas):.4f}")
print(f"Maximum possible cut: {len(edges)} edges")Connecting to the Registry
The VQE ansatz circuit and QAOA MaxCut circuit in the quantumcomputer.dev registry are stored as OpenQASM. You can view and simulate them in the browser playground, or export them to PennyLane format using the Export button on each circuit's detail page.
To load an OpenQASM circuit from the registry into PennyLane:
import pennylane as qml
# OpenQASM string exported from the registry
qasm_string = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0], q[1];
measure q[0] -> c[0];
measure q[1] -> c[1];
"""
tape = qml.from_qasm(qasm_string)
dev = qml.device("default.qubit", wires=2)
qnode = qml.QNode(tape, dev)
print(qml.draw(qnode)())qml.from_qasm() converts OpenQASM 2.0 to a PennyLane tape, which can be executed directly or extended with additional layers for variational optimization.
Devices and Scale
Device | Speed | Notes |
|---|---|---|
| Slow (Python) | Reference implementation, all operations supported |
| Fast (C++) | Recommended for development up to ~25 qubits |
| Very fast (CUDA) | Requires NVIDIA GPU, up to ~30 qubits |
| Fast (C++) | Qiskit Aer backend, supports noise models |
| Hardware | Amazon Braket quantum hardware |
| Hardware | IBM Quantum hardware |
For most development work, lightning.qubit is the right choice. It is installed with pip install pennylane-lightning and is a drop-in replacement for default.qubit with no API changes.
When to Use PennyLane vs Qiskit
Use PennyLane when your circuit has tunable parameters and you need gradients (VQE, QAOA, quantum machine learning, parameter optimization). PennyLane's gradient infrastructure is its primary differentiator.
Use Qiskit when you need noise-aware simulation that accurately matches a specific IBM device, or when your workflow is primarily targeted at IBM hardware and you want direct integration with Qiskit Runtime.
The two are not mutually exclusive: PennyLane's qiskit.aer and qiskit.ibmq devices let you run PennyLane circuits on Qiskit backends, combining PennyLane's gradient tools with Qiskit's noise models and hardware access.