Qiskit Beginner Free 18/61 in series 30 min read

Qiskit Basics: Quantum Gates, Circuits, and Measurements

A comprehensive beginner guide to Qiskit covering single-qubit gates, two-qubit gates, circuit drawing, statevector simulation, measurement, and histograms, with 20+ code examples.

What you'll learn

  • Qiskit
  • gates
  • circuits
  • measurement
  • X gate
  • H gate
  • CNOT
  • statevector

Prerequisites

  • Basic Python (variables, functions, loops)
  • No quantum physics background needed

This tutorial is your complete reference for Qiskit fundamentals. You will work through every common gate type, learn to build and visualize circuits, simulate quantum states, and extract measurement results. Each concept is shown with runnable code.

Setup

pip install qiskit qiskit-aer matplotlib pylatexenc
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
import numpy as np

Creating Circuits

Basic Circuit Creation

# 1 qubit, no classical bits
qc = QuantumCircuit(1)

# 2 qubits, 2 classical bits
qc = QuantumCircuit(2, 2)

# Named circuit
qc = QuantumCircuit(3, 3, name="my_circuit")
print(qc.name)  # 'my_circuit'
print(qc.num_qubits)  # 3

Circuit with Registers

For larger circuits, named registers make it easier to track qubits:

from qiskit import QuantumRegister, ClassicalRegister

q = QuantumRegister(3, name="q")
c = ClassicalRegister(3, name="c")
qc = QuantumCircuit(q, c)
print(qc.draw())

Single-Qubit Gates

X Gate (Pauli-X / Quantum NOT)

The X gate flips |0⟩ to |1⟩ and |1⟩ to |0⟩:

qc = QuantumCircuit(1, 1)
qc.x(0)           # Apply X gate to qubit 0
qc.measure(0, 0)
print(qc.draw())
# Starting in |0⟩, measuring gives '1' every time

Y Gate (Pauli-Y)

Y combines a bit flip and a phase flip:

qc = QuantumCircuit(1, 1)
qc.y(0)
qc.measure(0, 0)
# Also gives '1' from |0⟩, but with complex phase on the state vector

Z Gate (Pauli-Z / Phase Flip)

Z leaves |0⟩ unchanged and flips the phase of |1⟩:

qc = QuantumCircuit(1, 1)
qc.z(0)           # No visible effect on |0⟩ measured in computational basis
qc.measure(0, 0)
# Measurement gives '0' -- Z on |0⟩ does nothing observable
# But Z on |1⟩ introduces a phase that matters in superposition

qc2 = QuantumCircuit(1, 1)
qc2.x(0)          # Put in |1⟩
qc2.z(0)          # Phase flip (invisible in this basis)
qc2.x(0)          # Flip back to |0⟩
qc2.measure(0, 0)

Hadamard Gate

The Hadamard gate creates equal superposition:

qc = QuantumCircuit(1, 1)
qc.h(0)
qc.measure(0, 0)
# Measuring gives '0' and '1' roughly equally across many shots

S Gate (Phase Gate, √Z)

S applies a 90-degree phase rotation to |1⟩:

qc = QuantumCircuit(1, 1)
qc.h(0)   # Create superposition
qc.s(0)   # Apply S gate
qc.h(0)   # Interfere back
qc.measure(0, 0)

Two S gates equal one Z gate: qc.s(0); qc.s(0) is equivalent to qc.z(0).

T Gate (π/8 Gate, ⁴√Z)

T applies a 45-degree phase rotation:

qc = QuantumCircuit(1, 1)
qc.h(0)
qc.t(0)
qc.measure(0, 0)

# Inverse T gate
qc2 = QuantumCircuit(1)
qc2.tdg(0)   # T-dagger: inverse of T

Two T gates equal one S gate. Four T gates equal one Z gate.

S-dagger and T-dagger

Every gate in quantum computing must be reversible. The Hermitian conjugate (dagger) of S and T are:

qc = QuantumCircuit(1)
qc.sdg(0)    # S-dagger: inverse of S
qc.tdg(0)    # T-dagger: inverse of T

Rotation Gates

Rotation gates rotate around a Bloch sphere axis by an arbitrary angle:

import numpy as np

qc = QuantumCircuit(1)

# Rotation around X axis by angle theta
qc.rx(np.pi / 4, 0)      # 45-degree rotation around X

# Rotation around Y axis
qc.ry(np.pi / 3, 0)      # 60-degree rotation around Y

# Rotation around Z axis
qc.rz(np.pi / 2, 0)      # 90-degree rotation around Z (equivalent to S)

# General rotation (3 angles: phi, theta, lam)
qc.u(np.pi/4, 0, np.pi/2, 0)   # U gate: most general single-qubit gate

Drawing Circuits

Qiskit has multiple drawing modes:

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

# Text drawing (works in any terminal)
print(qc.draw('text'))

# Matplotlib drawing (saves to PNG)
fig = qc.draw('mpl')
fig.savefig('circuit.png')

# LaTeX drawing (requires pylatexenc)
print(qc.draw('latex_source'))

Two-Qubit Gates

CNOT (Controlled-NOT / CX)

The CNOT flips the target qubit if the control qubit is |1⟩:

qc = QuantumCircuit(2, 2)
qc.x(0)          # Set qubit 0 to |1⟩
qc.cx(0, 1)      # CNOT: control=0, target=1
qc.measure([0, 1], [0, 1])
# Result is always '11' -- control is 1 so target flips to 1
# Control is |0⟩: target unchanged
qc2 = QuantumCircuit(2, 2)
qc2.cx(0, 1)     # Control is |0⟩ by default
qc2.measure([0, 1], [0, 1])
# Result is always '00' -- control is 0, no flip

CZ (Controlled-Z)

CZ applies a Z gate to the target if the control is |1⟩:

qc = QuantumCircuit(2)
qc.h(0)
qc.h(1)
qc.cz(0, 1)   # CZ gate
qc.h(0)
qc.h(1)
# CZ in Hadamard basis is equivalent to CNOT

SWAP

SWAP exchanges the states of two qubits:

qc = QuantumCircuit(2, 2)
qc.x(0)           # qubit 0 = |1⟩, qubit 1 = |0⟩
qc.swap(0, 1)     # Exchange: qubit 0 = |0⟩, qubit 1 = |1⟩
qc.measure([0, 1], [0, 1])
# Result: '10' (qubit 1 is now 1, qubit 0 is now 0)

Controlled Versions of Any Gate

Qiskit lets you create controlled versions of any gate:

from qiskit.circuit.library import HGate

qc = QuantumCircuit(2)
ch = HGate().control(1)   # Controlled-H
qc.append(ch, [0, 1])     # control=0, target=1
print(qc.draw())

CCX (Toffoli Gate)

The Toffoli gate has two control qubits and one target:

qc = QuantumCircuit(3, 3)
qc.x(0)           # control 0 = |1⟩
qc.x(1)           # control 1 = |1⟩
qc.ccx(0, 1, 2)  # Target qubit 2 flips only when BOTH controls are |1⟩
qc.measure([0, 1, 2], [0, 1, 2])
# Result: '111'

Barriers

Barriers are visual separators in circuit diagrams. They also prevent the compiler from rearranging gates across the barrier:

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.h(1)
qc.barrier()        # Draw a vertical separator
qc.cx(0, 1)
qc.barrier(0)       # Barrier only on qubit 0
qc.measure([0, 1], [0, 1])
print(qc.draw())

Statevector Simulation

Statevector simulation computes the exact quantum state without measurement noise:

from qiskit_aer import AerSimulator

# Build a circuit without measurement
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.save_statevector()

simulator = AerSimulator(method='statevector')
compiled = transpile(qc, simulator)
result = simulator.run(compiled).result()

sv = result.get_statevector()
print(sv)
# Statevector([0.70710678+0.j, 0.        +0.j, 0.        +0.j,
#              0.70710678+0.j],
#             dims=(2, 2))
# Index 0 = |00⟩, index 3 = |11⟩: Bell state confirmed

Inspecting Amplitudes

# Get the amplitude for each basis state
n_qubits = 2
for i, amp in enumerate(sv):
    label = format(i, f'0{n_qubits}b')
    prob = abs(amp) ** 2
    if prob > 0.001:
        print(f"|{label}⟩: amplitude={amp:.3f}, probability={prob:.3f}")
# |00⟩: amplitude=0.707+0.000j, probability=0.500
# |11⟩: amplitude=0.707+0.000j, probability=0.500

Measurement and Shot-Based Simulation

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

simulator = AerSimulator()
compiled = transpile(qc, simulator)
result = simulator.run(compiled, shots=2000).result()

counts = result.get_counts()
print(counts)
# Output: {'00': ~1000, '11': ~1000}

Accessing Individual Measurement Results

result = simulator.run(compiled, shots=10, memory=True).result()
memory = result.get_memory()
print(memory)  # Output: ['00', '11', '00', '11', '00', '11', '11', '00', '11', '00'] (varies)
# Each entry is one shot result

Plotting Histograms

from qiskit.visualization import plot_histogram
import matplotlib.pyplot as plt

fig = plot_histogram(counts, title="Bell State Measurement")
fig.savefig("histogram.png")
plt.show()

Comparing Multiple Results

# Run two different circuits and compare
counts_a = {'00': 510, '11': 490}
counts_b = {'00': 485, '11': 515}

fig = plot_histogram(
    [counts_a, counts_b],
    legend=["Run 1", "Run 2"],
    title="Comparison"
)
fig.savefig("comparison.png")

Conditional Measurement (If-Else)

Qiskit supports classical conditioning: applying a gate only if a classical bit has a certain value:

qc = QuantumCircuit(2, 1)
qc.h(0)
qc.measure(0, 0)

# Only apply X to qubit 1 if classical bit 0 is 1
with qc.if_test((qc.clbits[0], 1)):
    qc.x(1)

qc.draw('text')

Circuit Composition

You can combine circuits:

# Build sub-circuits
prep = QuantumCircuit(2)
prep.h(0)
prep.cx(0, 1)

measure_block = QuantumCircuit(2, 2)
measure_block.measure([0, 1], [0, 1])

# Combine them
full = prep.compose(measure_block)
print(full.draw())

Circuit Properties

qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.ccx(0, 1, 2)

print(qc.num_qubits)    # 3
print(qc.depth())        # Circuit depth (longest path)
print(qc.count_ops())    # Output: OrderedDict({'h': 1, 'cx': 1, 'ccx': 1})
print(qc.size())         # Total number of gates

Transpilation

Real hardware only supports a limited native gate set. Transpilation converts your circuit to use only those gates:

from qiskit import transpile

# Simulate transpilation for a generic basis
compiled = transpile(qc, basis_gates=['cx', 'u'], optimization_level=3)
print(compiled.count_ops())  # Only cx and u gates
print(compiled.depth())       # May be different from original

Optimization levels range from 0 (minimal) to 3 (aggressive optimization):

for level in [0, 1, 2, 3]:
    c = transpile(qc, basis_gates=['cx', 'u'], optimization_level=level)
    print(f"Level {level}: depth={c.depth()}, gates={c.size()}")

A Complete Example: GHZ State

The GHZ (Greenberger-Horne-Zeilinger) state is a three-qubit generalization of the Bell state:

|GHZ⟩ = (|000⟩ + |111⟩) / √2
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram
import matplotlib.pyplot as plt

# Build GHZ state circuit
ghz = QuantumCircuit(3, 3)
ghz.h(0)         # Superposition on qubit 0
ghz.cx(0, 1)     # Entangle qubits 0 and 1
ghz.cx(0, 2)     # Entangle qubits 0 and 2
ghz.barrier()
ghz.measure([0, 1, 2], [0, 1, 2])

print(ghz.draw())

# Simulate
simulator = AerSimulator()
compiled = transpile(ghz, simulator)
result = simulator.run(compiled, shots=2000).result()
counts = result.get_counts()
print(counts)
# Output: {'000': ~1000, '111': ~1000}

# Plot
fig = plot_histogram(counts, title="GHZ State")
fig.savefig("ghz.png")
plt.show()

Summary: Gate Reference

GateMethodDescription
Xqc.x(q)Bit flip (NOT gate)
Yqc.y(q)Bit + phase flip
Zqc.z(q)Phase flip
Hqc.h(q)Hadamard (superposition)
Sqc.s(q)90-degree phase
Tqc.t(q)45-degree phase
Rxqc.rx(θ, q)X-axis rotation
Ryqc.ry(θ, q)Y-axis rotation
Rzqc.rz(θ, q)Z-axis rotation
CNOTqc.cx(ctrl, tgt)Controlled-NOT
CZqc.cz(ctrl, tgt)Controlled-Z
SWAPqc.swap(q0, q1)Swap two qubits
CCXqc.ccx(c0, c1, tgt)Toffoli (two controls)

You now have the toolkit to build, visualize, simulate, and analyze quantum circuits in Qiskit. These primitives appear in every quantum algorithm: Grover’s search, quantum teleportation, variational algorithms, and quantum error correction all build on exactly these gates and patterns.

Was this tutorial helpful?