Build Quantum Circuits with Qiskit
Build quantum circuits in Python to prove superposition and entanglement.
Introduction
30 Second Summary
When you flip a coin, it lands heads or tails. A quantum coin can be both at once, and that strange behaviour is just linear algebra in disguise.
In this project, you will build quantum circuits in Python that put qubits into superposition, entangle them, and demonstrate genuine quantum advantage. You will go from "what is a qubit?" to proving entanglement is real through runnable code.
What You'll Build
Imagine running a cell in your notebook and watching a histogram prove that two qubits are perfectly correlated, even though each one individually looks completely random.
By the end of this project, you'll have:
- A single-qubit circuit that puts a qubit into superposition and shows genuinely random 50/50 measurement outcomes.
- A two-qubit Bell state circuit whose histogram proves entanglement is real by showing correlations that are impossible in classical physics.
- An interference circuit that uses quantum phases to produce a correct answer in one step instead of two, demonstrating real quantum advantage.
- Secret Mission: implement the full Deutsch algorithm to prove quantum computers can solve certain problems faster than any classical approach.
Are there any prerequisites?
You should be comfortable with basic vectors and matrices (what a 2D vector looks like, how matrix multiplication works). You also need a Google account to access Google Colab, which runs everything in your browser with zero local setup.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
You're going to take the linear algebra you already know and use it to build real quantum circuits in Python. By the end, you'll have a shareable notebook that proves quantum entanglement is real through code you wrote yourself.
Set Up Your Quantum Lab
To build quantum circuits, you need a Python environment with a quantum computing library installed. Writing circuits in code lets you connect the math (vectors, matrices) directly to quantum behaviour.
Google Colab gives you a Python environment that runs entirely in your browser. There is nothing to install on your computer. You will use Qiskit, the most widely used quantum computing SDK, to create and simulate your circuits.
In this step, get ready to:
- Open Google Colab and create a new notebook.
- Install Qiskit and verify the installation.
- Test visualization to confirm everything works.
Open Google Colab and create a new notebook
Google Colab is a free browser-based notebook environment that lets you write and run Python code without installing anything locally.
- Navigate to colab.research.google.com in your browser.
- Sign in with your Google account if prompted.
- Click New Notebook in the pop-up window. If no pop-up appears, click File then New notebook.
- Click the notebook title at the top left (it says something like Untitled0.ipynb).
- Rename it to Quantum Computing Fundamentals.
You should see a blank notebook with one empty code cell ready for input.
Don't see a New Notebook option?
Make sure you are signed into your Google account. Colab requires a Google login to create notebooks.
If the page looks blank, try a different browser or disable ad-blocking extensions that might interfere with Google services. Help me access Google Colab
Install Qiskit and verify the installation
Qiskit is a Python SDK for quantum computing. You also need qiskit-aer, a simulator that runs quantum circuits on your computer instead of real quantum hardware.
- In the first code cell of your notebook, install both packages by pasting and running this command:
!pip install 'qiskit[visualization]' qiskit-aer
- Click the play button to the left of the cell (or press Shift+Enter) to run it.
You will see installation output scrolling through the cell. This takes about 30-60 seconds.
What does this command do?
The ! prefix tells Colab to run a shell command instead of Python code.
The 'qiskit[visualization]' part installs Qiskit along with its optional visualization dependencies (like matplotlib for drawing circuits). The qiskit-aer part installs the quantum circuit simulator.
Seeing red error text during install?
Some red warning text about dependency conflicts is normal and can be ignored as long as the cell finishes running. If the cell ends with an actual error (like ModuleNotFoundError), try running the cell again. Help me fix my Qiskit installation in Colab
Now verify that Qiskit installed correctly by importing the modules you will use throughout this project.
- Click + Code in the toolbar to add a new code cell below.
- Paste this code and run it:
import qiskit
from qiskit import QuantumCircuit
from qiskit.visualization import plot_histogram
from qiskit.quantum_info import Statevector
from qiskit_aer import AerSimulator
import numpy as np
print(f"Qiskit version: {qiskit.__version__}")
sim = AerSimulator()
What does this code do?
- QuantumCircuit is the class you will use to build every quantum circuit.
- plot_histogram visualizes measurement results as bar charts.
- Statevector lets you inspect the exact quantum state (the vector) before measurement.
- AerSimulator simulates running circuits with realistic shot-based measurements, just like real quantum hardware would behave.
- sim = AerSimulator() creates a simulator instance you will reuse in every step.
You should see output like Qiskit version: 2.5.0 (your minor version may differ, but it should start with 2.).
Seeing ImportError or ModuleNotFoundError?
This usually means the install cell did not finish successfully. Go back to Cell 1 and run it again. Wait for it to fully complete before running Cell 2.
If you still see errors after reinstalling, click Runtime then Restart runtime, then run Cell 1 followed by Cell 2 again. Help me fix import errors for Qiskit
Test that visualization works
The final check is confirming that Qiskit can draw circuit diagrams. This uses the matplotlib visualization backend you installed with the [visualization] extra.
- Add a new code cell below by clicking + Code.
- Before you run the next cell, what do you think a one-qubit circuit with a single H gate will look like as a diagram?
- Paste this code and run it:
# Test that visualization works
test_circuit = QuantumCircuit(1)
test_circuit.h(0)
test_circuit.draw('mpl')
What does this code do?
QuantumCircuit(1) creates a circuit with one qubit. The .h(0) method adds a Hadamard gate to qubit 0 (you will learn what this gate does in Step 3).
.draw('mpl') renders the circuit as a matplotlib image directly in your notebook.
You should see a circuit diagram with a single horizontal line (the qubit) and a box labelled H on it.
No image appearing?
Make sure the cell's last line is test_circuit.draw('mpl') with no print() around it. Colab automatically displays the output of the last expression in a cell.
If you see a text-based diagram instead of an image, the visualization extra may not have installed correctly. Re-run Cell 1 and then restart the runtime. Help me get circuit visualization working in Colab
✔️ Awesome, I've got everything!
Great. Make sure all three cells are saved in order in your notebook.
ⓧ I'd like to double check the full code
Here are all three cells you should have in your notebook. Confirm each one matches.
Cell 1:
!pip install 'qiskit[visualization]' qiskit-aer
Cell 2:
import qiskit
from qiskit import QuantumCircuit
from qiskit.visualization import plot_histogram
from qiskit.quantum_info import Statevector
from qiskit_aer import AerSimulator
import numpy as np
print(f"Qiskit version: {qiskit.__version__}")
sim = AerSimulator()
Cell 3:
# Test that visualization works
test_circuit = QuantumCircuit(1)
test_circuit.h(0)
test_circuit.draw('mpl')
Your quantum lab is ready. Next up, you will create your first qubit and see how quantum states map to the vectors you already know from linear algebra.
Your First Qubit - Vectors in Action
Your quantum lab is ready. Now it is time to answer the most fundamental question: what actually IS a qubit?
A qubit is a 2D vector. That is not a simplification or an analogy. It is literally a column vector with two entries. You already know how vectors work from linear algebra. In this step, you will see that connection in running code and produce your first quantum measurement result.
In this step, get ready to:
- Create and measure a qubit in state |0>, seeing that it always produces 0.
- Inspect the raw state vector to see the qubit's amplitudes.
- Apply the X gate (a matrix) to flip the qubit and compare the results.
Create a single-qubit circuit and measure it
Every qubit starts in state |0>, which is the vector [1, 0]. This is the quantum equivalent of a classical bit set to 0. If you measure it without doing anything else, it should always return 0.
Before you run this, what do you think the histogram will show? If a qubit starts as [1, 0] and you measure it 1024 times, will you see any 1s?
- Add a new code cell below your existing cells. Create a single-qubit circuit, measure it, simulate it with 1024 shots, and plot the result by pasting this code:
# A qubit starts in state |0> - the vector [1, 0]
qc = QuantumCircuit(1, 1)
qc.measure(0, 0)
# Simulate 1024 measurements
result = sim.run(qc, shots=1024).result()
counts = result.get_counts()
print(f"Measurement results: {counts}")
plot_histogram(counts)
What does this code do?
Here is what each piece accomplishes:
- QuantumCircuit(1, 1) creates a circuit with 1 qubit and 1 classical bit (to store the measurement result).
- qc.measure(0, 0) measures qubit 0 and stores the outcome in classical bit 0.
- sim.run(qc, shots=1024) runs the circuit 1024 times on the simulator. Each run is like flipping a coin (except this coin is rigged to always land on 0).
- Run the cell by pressing the play button or pressing Shift+Enter.
You should see {'0': 1024} printed, followed by a histogram bar showing 100% of measurements are "0". The qubit has zero probability of being "1" because its amplitude for the |1> state is 0.
Not seeing the histogram?
If you see an error about sim not being defined, make sure you ran Cell 2 (the imports cell) first. Colab cells must run in order.
If the histogram does not render, try running the cell again. Sometimes Colab needs a moment for matplotlib to initialise.
help me debug my qubit measurement cell
Inspect the state vector directly
The histogram shows measurement outcomes, but you can also look at the raw quantum state. This is the actual vector that represents your qubit.
- Add a new code cell below Cell 4. Use the Statevector class to inspect the qubit's vector representation by pasting this code:
# See the actual quantum state (the vector)
qc_no_measure = QuantumCircuit(1)
state = Statevector.from_instruction(qc_no_measure)
print(f"|0> state vector: {state.data}")
print(f"Probabilities: {state.probabilities()}")
What does this code do?
- Statevector.from_instruction() extracts the mathematical state of the qubit as a vector. No measurement needed.
- state.data gives you the raw amplitudes. For |0>, this is [1+0j, 0+0j]. The two numbers are the amplitudes for the |0> and |1> outcomes.
- state.probabilities() shows the probability of each outcome. Probability = amplitude squared.
- Run the cell.
You should see [1.+0.j 0.+0.j] for the state vector, and [1. 0.] for the probabilities. The qubit in state |0> is literally the vector [1, 0]. One hundred percent probability of measuring 0, zero percent probability of measuring 1.
What are amplitudes?
A qubit's state is defined by two complex numbers called amplitudes. To get the probability of measuring a particular outcome, you square the amplitude's magnitude.
For |0> = [1, 0]: probability of 0 is |1|² = 1, probability of 1 is |0|² = 0. This is why your histogram was 100% zeros.
Create a qubit in state |1> and compare
Now that you have seen |0> = [1, 0], the natural question is: how do you get |1> = [0, 1]? You apply a matrix that swaps the two amplitudes. In quantum computing, this matrix is called the X gate.
Before you run this next cell, predict: if the X gate matrix is [[0,1],[1,0]] and it acts on [1,0], what vector do you get? What will the histogram show?
- Add a new code cell below Cell 5. Apply the X gate to flip the qubit from |0> to |1> and verify both the measurement and the state vector by pasting this code:
# The X gate flips |0> to |1>
# X gate matrix: [[0, 1], [1, 0]]
qc_x = QuantumCircuit(1, 1)
qc_x.x(0)
qc_x.measure(0, 0)
result = sim.run(qc_x, shots=1024).result()
counts = result.get_counts()
print(f"After X gate: {counts}")
# Verify the state vector
qc_x_state = QuantumCircuit(1)
qc_x_state.x(0)
state = Statevector.from_instruction(qc_x_state)
print(f"|1> state vector: {state.data}")
print(f"X gate is the matrix [[0,1],[1,0]] - it swaps the amplitudes")
plot_histogram(counts)
What does this code do?
- qc_x.x(0) applies the X gate (the quantum NOT gate) to qubit 0. This multiplies the state vector by the matrix [[0,1],[1,0]], swapping the amplitudes.
- The cell then measures and simulates, just like Cell 4. It also creates a separate circuit without measurement to inspect the state vector directly.
- Both approaches (histogram + state vector) confirm the same thing: the qubit is now 100% in state |1>.
- Run the cell.
You should see {'1': 1024} in the measurement results and a state vector of [0.+0.j 1.+0.j]. The X gate swapped [1, 0] into [0, 1]. The histogram is now 100% ones.
Why does the X gate work this way?
Matrix multiplication tells the whole story. The X gate is [[0,1],[1,0]]. Multiply it by the vector [1, 0] and you get [0, 1]. This is the exact same linear algebra you already know, applied to quantum states.
Every quantum gate is a matrix. Every qubit state is a vector. Quantum computing is linear algebra running on hardware that physically holds these vectors.
Seeing unexpected counts?
If your histogram shows anything other than 100% ones, make sure you typed qc_x.x(0) (lowercase x) and not a different gate letter.
If you see a NameError for sim, go back and run Cell 2 first.
help me fix my X gate circuit
✔️ Awesome, I've got everything!
Great. Make sure all three cells (4, 5, and 6) are saved in your notebook before moving on.
ⓧ I'd like to double check the full code
Here are the three cells you added in this step. Scroll through your notebook and confirm each one matches.
Cell 4:
# A qubit starts in state |0> - the vector [1, 0]
qc = QuantumCircuit(1, 1)
qc.measure(0, 0)
# Simulate 1024 measurements
result = sim.run(qc, shots=1024).result()
counts = result.get_counts()
print(f"Measurement results: {counts}")
plot_histogram(counts)
Cell 5:
# See the actual quantum state (the vector)
qc_no_measure = QuantumCircuit(1)
state = Statevector.from_instruction(qc_no_measure)
print(f"|0> state vector: {state.data}")
print(f"Probabilities: {state.probabilities()}")
Cell 6:
# The X gate flips |0> to |1>
# X gate matrix: [[0, 1], [1, 0]]
qc_x = QuantumCircuit(1, 1)
qc_x.x(0)
qc_x.measure(0, 0)
result = sim.run(qc_x, shots=1024).result()
counts = result.get_counts()
print(f"After X gate: {counts}")
# Verify the state vector
qc_x_state = QuantumCircuit(1)
qc_x_state.x(0)
state = Statevector.from_instruction(qc_x_state)
print(f"|1> state vector: {state.data}")
print(f"X gate is the matrix [[0,1],[1,0]] - it swaps the amplitudes")
plot_histogram(counts)
You have just proven that qubits are vectors and gates are matrices. But so far, your qubits behave exactly like classical bits. They are either 0 or 1, nothing in between. Next up, you will put a qubit into superposition, where it genuinely exists in both states at once, and see what happens when you measure it.
Superposition - The Quantum Coin Flip
In the last step you saw qubits as vectors with definite values. The qubit was always |0> or |1>, never anything in between.
Now you'll do something impossible with classical bits: put a qubit into a state where it is genuinely in both 0 and 1 simultaneously. You'll use the Hadamard gate, a specific matrix that creates superposition. The result looks like a coin flip, but by the end of this step you'll prove something deeper is going on.
In this step, get ready to:
- Apply the Hadamard gate and see superposition measurement results.
- Inspect the state vector to see the math behind superposition.
- Compose gates to prove they are reversible matrix operations.
Apply the Hadamard gate and observe superposition
The Hadamard gate (H gate) is a matrix that transforms a definite state into an equal combination of both states. It is the most important single-qubit gate in quantum computing.
- Add a new code cell below Cell 6 and paste the following code to create a circuit with the H gate, simulate it, and inspect the state vector:
# The Hadamard gate creates superposition
# H matrix: (1/sqrt(2)) * [[1, 1], [1, -1]]
qc_h = QuantumCircuit(1, 1)
qc_h.h(0)
qc_h.measure(0, 0)
result = sim.run(qc_h, shots=1024).result()
counts = result.get_counts()
print(f"Superposition measurement: {counts}")
print("Roughly 50/50 - like a coin flip!")
# See the state vector
qc_h_state = QuantumCircuit(1)
qc_h_state.h(0)
state = Statevector.from_instruction(qc_h_state)
print(f"\nState vector after H: {state.data}")
print(f"Each amplitude is 1/sqrt(2) = {1/np.sqrt(2):.4f}")
print(f"Probability of |0>: |{state.data[0]:.4f}|^2 = {abs(state.data[0])**2:.4f}")
print(f"Probability of |1>: |{state.data[1]:.4f}|^2 = {abs(state.data[1])**2:.4f}")
plot_histogram(counts)
What does this code do?
This cell does two things: it measures the qubit in superposition (showing probabilistic outcomes), and it inspects the raw state vector (showing the exact amplitudes).
- qc_h.h(0) applies the Hadamard gate to qubit 0. The H gate matrix is (1/sqrt(2)) * [[1,1],[1,-1]].
- The matrix transforms [1, 0] into [1/sqrt(2), 1/sqrt(2)]. Each amplitude squared gives probability 1/2.
- The second section creates a separate circuit without measurement so Statevector.from_instruction() can read the exact amplitudes before the qubit "chooses" an outcome.
- Run the cell.
You should see a histogram showing approximately 50% "0" and 50% "1" (roughly 512 each, with some variation). The state vector output shows [0.707+0j, 0.707+0j] confirming each amplitude is 1/sqrt(2).
Seeing all zeros or all ones?
- Make sure you have qc_h.h(0) before qc_h.measure(0, 0). If the H gate comes after the measurement, it has no effect.
- Confirm your Cell 2 ran successfully and sim is available. If you see a NameError, re-run Cell 2 first.
Help me debug my Hadamard gate circuit in Qiskit
Verify with the statevector
Look at the state vector output from the cell you just ran. The values [0.707+0j, 0.707+0j] tell you something profound about what the qubit is BEFORE you measure it.
What is superposition really?
Before measurement, the qubit is in BOTH states simultaneously. It is not secretly 0 or 1 waiting to be revealed. It is genuinely both until the act of measurement forces it to choose.
This looks indistinguishable from a classical coin flip right now. A fair coin also gives 50/50 results. The difference is that a coin is always heads or tails before you look. A qubit in superposition is truly neither until you measure. You will prove this difference in the next step using entanglement.
Compose multiple gates and predict the outcome
If the H gate creates randomness by putting a qubit into superposition, what happens if you apply it twice? If it were truly random, you would get random results again.
- Before you run any code, predict: if you apply the H gate to a qubit in state |0> and then apply H again, will the result be random (50/50) or deterministic (100% one outcome)?
Think in matrices
Gates are matrices. Applying two gates in sequence is matrix multiplication. What is H * H? If the H matrix multiplied by itself equals the identity matrix, the qubit returns to its original state.
- Add a new code cell below Cell 7 and paste this code to test your prediction:
# What happens when we apply H twice?
# H * H = Identity matrix - the qubit returns to |0>
qc_hh = QuantumCircuit(1, 1)
qc_hh.h(0)
qc_hh.h(0)
qc_hh.measure(0, 0)
result = sim.run(qc_hh, shots=1024).result()
counts = result.get_counts()
print(f"H applied twice: {counts}")
print("100% |0> - the gates cancelled out!")
print("This proves: H*H = I (identity matrix)")
print("Gates are reversible matrix operations, not random processes.")
plot_histogram(counts)
What does this prove?
- H * H = Identity matrix, so applying H twice returns the qubit to its original state |0>.
- This proves that quantum gates are not random processes. They are precise, reversible matrix operations.
- The "randomness" only appears at the moment of measurement. The gate operations themselves are completely deterministic.
- Run the cell.
The histogram shows {'0': 1024}. Every single measurement is 0. The H gate applied twice is the identity matrix, returning the qubit exactly to its starting state.
Still seeing random results?
- Check that you have two qc_hh.h(0) lines, not just one. A single H creates superposition. Two H gates cancel each other out.
- Make sure both H gates come before the qc_hh.measure(0, 0) line.
Help me understand why H applied twice isn't giving me deterministic results
✔️ Awesome, I've got everything!
Great. Confirm you have saved both Cell 7 and Cell 8 in your notebook.
ⓧ I'd like to double check the full code
Here is Cell 7 (Superposition with Hadamard Gate):
# The Hadamard gate creates superposition
# H matrix: (1/sqrt(2)) * [[1, 1], [1, -1]]
qc_h = QuantumCircuit(1, 1)
qc_h.h(0)
qc_h.measure(0, 0)
result = sim.run(qc_h, shots=1024).result()
counts = result.get_counts()
print(f"Superposition measurement: {counts}")
print("Roughly 50/50 - like a coin flip!")
# See the state vector
qc_h_state = QuantumCircuit(1)
qc_h_state.h(0)
state = Statevector.from_instruction(qc_h_state)
print(f"\nState vector after H: {state.data}")
print(f"Each amplitude is 1/sqrt(2) = {1/np.sqrt(2):.4f}")
print(f"Probability of |0>: |{state.data[0]:.4f}|^2 = {abs(state.data[0])**2:.4f}")
print(f"Probability of |1>: |{state.data[1]:.4f}|^2 = {abs(state.data[1])**2:.4f}")
plot_histogram(counts)
Here is Cell 8 (H Gate Twice):
# What happens when we apply H twice?
# H * H = Identity matrix - the qubit returns to |0>
qc_hh = QuantumCircuit(1, 1)
qc_hh.h(0)
qc_hh.h(0)
qc_hh.measure(0, 0)
result = sim.run(qc_hh, shots=1024).result()
counts = result.get_counts()
print(f"H applied twice: {counts}")
print("100% |0> - the gates cancelled out!")
print("This proves: H*H = I (identity matrix)")
print("Gates are reversible matrix operations, not random processes.")
plot_histogram(counts)
You now have superposition working. A single H gate creates genuine quantum randomness, but applying it twice cancels out perfectly. The qubit is not actually random. The gates are precise matrix operations. Only measurement introduces randomness.
But here is the limitation: superposition alone looks exactly like flipping a coin. There is nothing in your histogram that a classical random number generator could not produce. Next up, you will create entanglement, which produces correlations that are impossible to explain classically.
Entanglement - The Quantum Magic
In the last step, you put a qubit in superposition and got random 50/50 results. That randomness looked exactly like flipping a coin. On its own, a random bit is not useful for computation.
Entanglement is what transforms quantum randomness into quantum power. You will create two qubits that are perfectly correlated in a way that is impossible to explain with classical physics.
In this step, get ready to:
- Create a Bell state circuit that entangles two qubits.
- Trace the math of entanglement step by step using state vectors.
- Prove entanglement is non-classical by measuring in a different basis.
Create a Bell state circuit (H + CNOT)
A Bell state is the simplest example of entanglement. It uses two gates: the Hadamard (H) you already know, and a new gate called CNOT (Controlled-NOT).
The CNOT gate has a "control" qubit and a "target" qubit. It flips the target ONLY when the control is in state |1>. When the control is in superposition, this creates entanglement.
Before you run this circuit, make a prediction. You are putting qubit 0 into superposition (50/50 random), then applying CNOT so qubit 1 depends on qubit 0. What measurement outcomes do you expect to see for the two qubits together?
- Add a new code cell in your Colab notebook (Cell 9) and enter the following code:
# Create a Bell state: H on qubit 0, then CNOT
# This entangles the two qubits
bell = QuantumCircuit(2, 2)
bell.h(0)
bell.cx(0, 1)
bell.measure([0, 1], [0, 1])
print("Bell State Circuit:")
display(bell.draw('mpl'))
result = sim.run(bell, shots=1024).result()
counts = result.get_counts()
print(f"\nBell state measurements: {counts}")
print("Only '00' and '11' appear - NEVER '01' or '10'!")
print("The qubits are perfectly correlated.")
plot_histogram(counts)
What does this code do?
- QuantumCircuit(2, 2) creates a circuit with 2 qubits and 2 classical bits for measurement results.
- bell.h(0) puts qubit 0 into superposition (the 50/50 state you saw in the previous step).
- bell.cx(0, 1) applies the CNOT gate with qubit 0 as control and qubit 1 as target. This is where entanglement happens.
- bell.measure([0, 1], [0, 1]) measures both qubits and stores the results in the classical bits.
- Run the cell.
You should see a circuit diagram followed by a histogram showing ONLY "00" and "11" (approximately 50% each). You will never see "01" or "10".
The two qubits are perfectly correlated. If qubit 0 is 0, qubit 1 is always 0. If qubit 0 is 1, qubit 1 is always 1. Each one individually looks random, but together they are perfectly locked.
Seeing all four outcomes (00, 01, 10, 11)?
Make sure the bell.cx(0, 1) line comes AFTER bell.h(0). The order matters. H creates superposition first, then CNOT entangles.
Also check that you are using bell.cx(0, 1) (control=0, target=1), not bell.cx(1, 0).
Why is my Bell state circuit showing all four outcomes?
Understand WHY this happens using the math
The histogram shows perfect correlation, but you should understand the mechanics. The state vector for two qubits is a 4-element vector (the tensor product of two 2D vectors). You can trace each gate's effect on this 4D vector to see exactly where entanglement emerges.
- Add a new code cell (Cell 10) and enter the following code to trace the Bell state math step by step:
# Let's trace the math step by step
print("=== Bell State Math ===\n")
# Step 1: Start state
qc_step = QuantumCircuit(2)
state = Statevector.from_instruction(qc_step)
print(f"Start |00>: {state.data}")
print(" = [1, 0, 0, 0] meaning 100% probability of |00>\n")
# Step 2: After H on qubit 0
qc_step.h(0)
state = Statevector.from_instruction(qc_step)
print(f"After H on q0: {np.round(state.data, 4)}")
print(" = [0.707, 0, 0.707, 0]")
print(" = (|00> + |10>) / sqrt(2)\n")
# Step 3: After CNOT
qc_step.cx(0, 1)
state = Statevector.from_instruction(qc_step)
print(f"After CNOT: {np.round(state.data, 4)}")
print(" = [0.707, 0, 0, 0.707]")
print(" = (|00> + |11>) / sqrt(2)")
print("\nThe CNOT flipped qubit 1 ONLY for the |10> part")
print("Result: qubits are now ENTANGLED")
What does the math reveal?
The two-qubit state vector has four entries: the amplitudes for |00>, |01>, |10>, and |11>.
- Start: [1, 0, 0, 0] means 100% probability of |00>. Both qubits are 0.
- After H: [0.707, 0, 0.707, 0] means equal probability of |00> and |10>. Qubit 0 is in superposition. Qubit 1 is still definitely 0.
- After CNOT: [0.707, 0, 0, 0.707] means equal probability of |00> and |11>. The CNOT flipped qubit 1 for the |10> part (making it |11>), but left the |00> part alone.
The final state cannot be written as two separate qubit states multiplied together. This is what makes it entangled.
- Run Cell 10.
You should see the state vector printed at each stage, confirming that the vector evolves from [1, 0, 0, 0] to [0.707, 0, 0.707, 0] to [0.707, 0, 0, 0.707].
Seeing unexpected values in the state vector?
Make sure you created a fresh circuit with qc_step = QuantumCircuit(2) at the top of this cell. If you reuse a circuit variable from an earlier cell, the gates accumulate.
My state vector values don't match the expected Bell state math.
Prove this is NOT classical correlation
A skeptic might say: "Maybe the qubits were secretly decided at creation. Like flipping two coins that are glued together." If that were true, the correlation would only hold in one measurement basis.
You can test this by measuring in the X basis (applying H before measuring) instead of the standard Z basis. Classical correlated coins would show all four outcomes in a different basis. What do you think entangled qubits will show?
- Add a new code cell (Cell 11) and enter the following code to test the Bell state in a different measurement basis:
# Is entanglement just "pre-decided" classical correlation?
# If coins were secretly decided at creation, measuring in a
# different basis would break the correlation.
# Test: measure in X basis (apply H before measuring)
bell_x = QuantumCircuit(2, 2)
bell_x.h(0)
bell_x.cx(0, 1)
# Change to X basis measurement
bell_x.h(0)
bell_x.h(1)
bell_x.measure([0, 1], [0, 1])
result = sim.run(bell_x, shots=1024).result()
counts = result.get_counts()
print("Measuring Bell state in X basis:")
print(f"Results: {counts}")
print("\nOnly '00' and '11' appear in X basis too!")
print("Classical correlated coins would show all four outcomes.")
print("This correlation in MULTIPLE bases is impossible classically.")
print("This is the essence of Bell's theorem.")
plot_histogram(counts)
What does this prove?
If the qubits were "pre-decided" (like two coins glued together), changing the measurement basis would destroy the correlation. You would see all four outcomes: 00, 01, 10, and 11.
Instead, you still see ONLY 00 and 11. Entangled qubits remain perfectly correlated no matter how you measure them. This correlation in multiple bases is impossible to explain classically. This is the essence of Bell's theorem.
- Run Cell 11.
You should see a histogram showing only "00" and "11" again. The correlation survives the change of measurement basis.
Seeing 01 or 10 in the X-basis results?
Check that you have bell_x.h(0) and bell_x.h(1) AFTER the bell_x.cx(0, 1) line and BEFORE the bell_x.measure() line. The H gates must come between the entanglement and the measurement to switch to the X basis.
My X-basis Bell state measurement is showing unexpected results.
✔️ Awesome, I've got everything!
Great. Double check you have saved all three cells (9, 10, and 11) in your notebook.
ⓧ I'd like to double check the full code
Scroll up through your notebook and confirm these three cells are present, in order:
- Cell 9: Bell State (Entanglement) - creates a 2-qubit circuit with H and CNOT, measures and plots the histogram.
- Cell 10: Bell State Math Walkthrough - traces the state vector at each stage using Statevector.from_instruction().
- Cell 11: Proving Entanglement is Non-Classical - measures the Bell state in the X basis by adding H gates before measurement.
You have created genuine quantum entanglement and proved it is non-classical. Next up, you will harness superposition and interference to make quantum computation actually useful. You will build a circuit that amplifies the correct answer and cancels the wrong ones.
Measuring Quantum Power - From Random to Useful
In the previous step, you proved that entanglement creates correlations that are impossible classically. That is genuinely impressive. But right now, both superposition and entanglement look like curiosities. Superposition gives you randomness. Entanglement gives you correlated randomness. Neither is useful for solving problems.
The missing ingredient is interference. By manipulating phases (signs on amplitudes), you can make wrong answers cancel out and right answers amplify. This is how every quantum algorithm works.
In this step, get ready to:
- Understand interference by building a circuit where amplitudes cancel to produce a deterministic outcome.
- Build an oracle circuit that identifies a function's property in a single query.
- Compare quantum and classical approaches to see genuine quantum advantage.
Understand interference: amplitudes can cancel
You already know that the H gate creates superposition: equal amplitudes for |0> and |1>. The Z gate does something subtler. It flips the phase of the |1> amplitude (multiplies it by -1) without changing any probabilities.
When you apply H again after a phase flip, the negative amplitude causes destructive interference. One outcome becomes impossible and the other becomes certain.
- Create a new code cell in your Colab notebook and paste the following code:
# Interference: amplitudes can ADD or CANCEL
# Circuit: H -> Z -> H
qc_int = QuantumCircuit(1, 1)
qc_int.h(0) # Create superposition: [0.707, 0.707]
qc_int.z(0) # Flip phase of |1>: [0.707, -0.707]
qc_int.h(0) # Interference: amplitudes cancel/add
qc_int.measure(0, 0)
print("Circuit: H -> Z -> H")
display(qc_int.draw('mpl'))
result = sim.run(qc_int, shots=1024).result()
counts = result.get_counts()
print(f"\nResult: {counts}")
print("100% |1>! The Z gate's phase flip caused interference.")
print("\nStep by step:")
print(" Start: [1, 0]")
print(" After H: [0.707, 0.707]")
print(" After Z: [0.707, -0.707] (phase flip on |1>)")
print(" After H: [0, 1] (destructive interference on |0>)")
print("\nKey insight: by manipulating PHASES we control the outcome.")
print("This is how quantum algorithms work!")
plot_histogram(counts)
What does this circuit do?
This circuit applies three gates in sequence: H, Z, then H again. Each transforms the state vector:
- H transforms [1, 0] into [0.707, 0.707]. The qubit is in superposition.
- Z flips the sign of the second amplitude: [0.707, -0.707]. The probabilities haven't changed (squaring -0.707 still gives 0.5), but the phase is different.
- The second H converts that phase difference into a probability difference. The amplitudes for |0> cancel (destructive interference) while the amplitudes for |1> add (constructive interference).
Before you run this cell, what do you think the histogram will show? A 50/50 split, all zeros, or all ones?
- Run the cell.
You should see a histogram showing 100% of measurements as "1". The circuit diagram appears above it, showing the H-Z-H sequence.
Why is this different from a coin flip?
In Step 3, superposition produced random 50/50 results. Here, three gates produce a completely deterministic outcome. The difference is that the Z gate introduced a phase that the second H gate converted into certainty.
Phases have no classical equivalent. A classical coin is either heads or tails. A quantum amplitude can be positive or negative, and that sign determines whether interference is constructive or destructive.
Seeing a 50/50 result instead of 100% ones?
- Make sure you have all three gates in order: .h(0), then .z(0), then .h(0) again. Missing the Z gate gives you H-H which returns to |0>, not |1>.
- Check that you used .z(0) (Pauli-Z) and not .x(0) (Pauli-X). The X gate flips the bit, while Z flips the phase.
Help me debug my interference circuit
Build a simple oracle circuit
You just saw that H-Z-H forces the qubit into |1> with certainty. Now reframe this as a computation. The Z gate in the middle is a "black box" (oracle) that marks a specific state with a phase flip.
The question you want to answer: is a function constant (same output for all inputs) or balanced (different outputs for different inputs)? Classically, you must query the function twice to compare outputs. With interference, one query is enough.
- Create a new code cell and paste the following code for the balanced oracle:
# Quantum advantage: determine a property in 1 step instead of 2
#
# Problem: Is a function f constant (same output for all inputs)
# or balanced (different outputs)?
# Classically: must check both inputs (2 evaluations)
# Quantumly: 1 evaluation using interference!
# Oracle for a BALANCED function (f(0)=0, f(1)=1)
# Implemented as a Z gate (flips phase of |1>)
qc_oracle = QuantumCircuit(1, 1)
qc_oracle.h(0) # Superposition: query both inputs at once
qc_oracle.z(0) # Oracle: marks |1> with a phase flip
qc_oracle.h(0) # Interference: reveals the answer
qc_oracle.measure(0, 0)
result = sim.run(qc_oracle, shots=1024).result()
counts = result.get_counts()
print("Oracle query result:")
print(f" {counts}")
print(" Measured |1> = function is BALANCED")
print("\n We determined this in 1 query!")
print(" Classically, we'd need to check f(0) AND f(1) = 2 queries")
What is the oracle doing?
The oracle is a Z gate that represents a balanced function (one that returns different values for different inputs). The H gates on either side create interference that reveals whether the oracle is balanced or constant.
If you measure |1>, the function is balanced. If you measure |0>, the function is constant. You get the answer in one query because superposition lets you evaluate both inputs simultaneously, and interference converts the global property into a single measurable bit.
Run and compare: quantum vs classical
To prove this works both ways, you need a constant function to compare against. A constant function that always returns 0 does nothing to the qubit. That means the oracle is simply the identity (no gate at all).
- Add the following code to the same cell, directly below the code you just pasted:
# Compare with constant function (identity = no gate)
qc_const = QuantumCircuit(1, 1)
qc_const.h(0) # Superposition
# No oracle gate = constant function (f(x) = 0 for all x)
qc_const.h(0) # Interference
qc_const.measure(0, 0)
result_const = sim.run(qc_const, shots=1024).result()
counts_const = result_const.get_counts()
print(f"\nConstant function result:")
print(f" {counts_const}")
print(" Measured |0> = function is CONSTANT")
print("\n Quantum speedup demonstrated!")
plot_histogram([counts, counts_const], legend=['Balanced', 'Constant'])
What does the constant oracle look like?
- A constant function (f(x) = 0 for all x) does nothing to the qubit's phase. No Z gate is applied.
- Without a phase flip, H followed by H equals the identity. The qubit returns to |0>.
- Measuring |0> tells you the function is constant. This is the same H-H cancellation you saw in Step 3.
Before you run this cell, predict: will the balanced and constant circuits produce the same measurement, or different ones?
- Run the cell.
You should see two distinct results. The balanced oracle produces {'1': 1024} and the constant oracle produces {'0': 1024}. The comparison histogram shows them side by side.
Why is this quantum advantage?
Classically, to determine whether a function is constant or balanced, you must evaluate it on both inputs and compare. That requires 2 queries. The quantum circuit determines the same property in 1 query.
This is the smallest possible demonstration of quantum speedup. Superposition lets you query both inputs at once. Interference converts a global property of the function (constant vs balanced) into a single deterministic measurement.
Seeing an error when running the cell?
- If you see a NameError for counts or counts_const, make sure both the balanced oracle code and the constant oracle code are in the same cell. The plot_histogram call at the end references both variables.
- If the histogram only shows one bar instead of a comparison, check that you passed a list to plot_histogram: [counts, counts_const] with square brackets.
Help me fix my oracle circuit
✔️ Awesome, I've got everything!
Great. Make sure Cell 13 is saved in your notebook.
ⓧ I'd like to double check the full code
Your Cell 13 should contain both sections in order:
- The balanced oracle section (comments explaining the problem, qc_oracle circuit with H-Z-H, simulation, and print statements).
- The constant oracle comparison section (qc_const circuit with H-H only, simulation, print statements, and the comparison plot_histogram call).
Scroll up and confirm both chunks are present in the same cell, in that order.
Secret mission
Implement the Full Deutsch Algorithm
In Step 5, you saw a simplified version of quantum advantage using a single qubit. The real Deutsch algorithm uses two qubits and a technique called phase kickback to classify any black-box function in a single query. Can you implement the full algorithm and prove it works for all four possible function types?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely in Google Colab, so there are no ongoing costs or paid resources to worry about. Your notebook is saved to your Google Drive automatically.
Resources you used:
- Google Colab notebook "Quantum Computing Fundamentals" (stored in your Google Drive).
- Installed Python packages (qiskit, qiskit-aer) in the Colab runtime. These are ephemeral and disappear when the runtime disconnects.
Keep everything running
No action needed. Choose this if you want to revisit or extend your quantum circuits later.
- Your notebook stays in Google Drive for free. Reopen it anytime at colab.research.google.com.
- Packages will need to be reinstalled when you reopen the notebook (Cell 1 handles this automatically).
- Use Colab's Share button to let others run your notebook and learn from your circuits.
Pause - I'll come back to this later
Since Colab notebooks have no running processes between sessions, there is nothing to pause. Your notebook is already idle when you close the browser tab.
- Close the Colab browser tab. The runtime shuts down automatically after a period of inactivity.
- Your notebook file remains in Google Drive at zero cost until you return.
Delete - I don't want to use this again
Remove the notebook from your Google Drive entirely.
- Open Google Drive at drive.google.com.
- Navigate to the Colab Notebooks folder.
- Right-click the "Quantum Computing Fundamentals" notebook and select Move to trash.
- Empty your trash to permanently delete the file. Click Trash in the left sidebar, then click Empty trash at the top.
- The installed packages (qiskit, qiskit-aer) were only in the ephemeral Colab runtime. They are already gone once the session ended.
That's a Wrap!
That's a Wrap!
Nice work! You just built quantum circuits from scratch, went from a single qubit measuring zero to proving that entangled particles are correlated in ways classical physics cannot explain.
You've learned how to:
- Build and simulate quantum circuits in Python using Qiskit and Google Colab, connecting qubits to vectors, gates to matrices, and measurement to probability from squared amplitudes.
- Create entanglement with a Bell state circuit and prove it is non-classical by measuring in different bases, showing correlations that no classical system can reproduce.
- Harness quantum interference to amplify correct answers and cancel wrong ones, demonstrating a circuit that solves a problem in one query where classical approaches need two.
- (Secret Mission) Implemented the full Deutsch algorithm with four oracle variants, proving provable quantum speedup using phase kickback and an ancilla qubit.
Ready to quiz yourself?