How to entangle two qubits

Published on May 14, 2026 in Quantum Computing  

In my previous article, I introduced you to the Hadamard gate which allows you to place a qubit in superposition.

We will now study the CNOT (Controlled-NOT) gate which allows the entanglement of two qubits.

This gate can be named in different ways, controlled-X gate, controlled-bit-flip gate, Feynman gate or controlled Pauli-X. For example, in Qiskit, you’ll call it with cx and it’ll be represented by a X in your quantum circuit drawing.

CNOT is a two-qubit quantum gate. It performs a NOT operation (X, inversion) on the target qubit only if the control qubit is 1. Otherwise it leaves the target unchanged.

  • The CNOT truth table is as follows:
  • |00⟩ => |00⟩
  • |01⟩ => |01⟩
  • |10⟩ => |11⟩
  • |11⟩ => |10⟩

With |control,target⟩ => output

Here is its matrix representation (with order |00⟩,|01⟩,|10⟩,|11⟩):

plaintext
CNOT = 
[[1, 0, 0, 0],
 [0, 1, 0, 0],
 [0, 0, 0, 1],
 [0, 0, 1, 0]]
  • In summary:
  • If control = 0: Nothing happens on the target.
  • If control = 1: the target is “flipped”.
  • For superposition qubits, it creates quantum correlations (entanglement).

With two qubits, by applying a hadamard gate to the first one, then a CNOT gate to our two qubits, we obtain the first Bell state. The Bell states correspond to the four states of maximum entanglement between qubits.

Here is the python code to simulate two qubits, with a Hadamard gate and a CNOT gate:

python
import math
import random
from typing import List, Tuple

IdentityMatrix = [[1+0j, 0+0j],
                  [0+0j, 1+0j]]

def tensor_product(a: List[List[complex]], b: List[List[complex]]) -> List[List[complex]]:
    # tensor product of two 2x2 matrices => 4x4
    result = [[0+0j for _ in range(4)] for _ in range(4)]
    for i in range(2):
        for j in range(2):
            for k in range(2):
                for l in range(2):
                    result[2*i + k][2*j + l] = a[i][j] * b[k][l]
    return result

class TwoQubitMatrix:
    def __init__(self, state: List[complex]):
        # basis order: |00>,|01>,|10>,|11>
        if len(state) != 4:
            raise ValueError("State must have 4 amplitudes.")
        self.state = [complex(x) for x in state]
        self._normalize()

    def _normalize(self):
        norm = math.sqrt(sum(abs(a)**2 for a in self.state))
        if norm == 0:
            raise ValueError("Zero state vector.")
        self.state = [a / norm for a in self.state]

    def amplitudes(self) -> Tuple[complex, complex, complex, complex]:
        return tuple(self.state)

    def apply_4x4(self, matrix: List[List[complex]]):
        self.state = [sum(matrix[i][j] * self.state[j] for j in range(4)) for i in range(4)]
        self._normalize()

    def apply_single_qubit(self, matrix: List[List[complex]], target: int):
        # target: 0 = left qubit (most significant), 1 = right qubit (least significant)
        if target == 0:
            big = tensor_product(matrix, IdentityMatrix)
        elif target == 1:
            big = tensor_product(IdentityMatrix, matrix)
        else:
            raise ValueError("target must be 0 or 1")
        self.apply_4x4(big)
        self._normalize()

    def measure_all(self) -> int:
        probs = [abs(a)**2 for a in self.state]
        r = random.random()
        cum = 0.0
        for i, p in enumerate(probs):
            cum += p
            if r < cum:
                collapsed = [0+0j]*4
                collapsed[i] = 1+0j
                self.state = collapsed
                return i
        # fallback
        self.state = [0+0j]*4
        self.state[3] = 1+0j
        return 3

# Hadamard gate
H = [[1/math.sqrt(2), 1/math.sqrt(2)],
     [1/math.sqrt(2), -1/math.sqrt(2)]]

# CNOT gate (control = qubit 0 (left), target = qubit 1 (right))
CNOT = [
    [1+0j, 0+0j, 0+0j, 0+0j],
    [0+0j, 1+0j, 0+0j, 0+0j],
    [0+0j, 0+0j, 0+0j, 1+0j],
    [0+0j, 0+0j, 1+0j, 0+0j],
]

if __name__ == "__main__":
    # prepare |00>
    q = TwoQubitMatrix([1+0j, 0+0j, 0+0j, 0+0j])
    # apply H on qubit 0 (left)
    q.apply_single_qubit(H, target=0)
    # apply CNOT (control = qubit 0, target = qubit 1)
    q.apply_4x4(CNOT)

    a00, a01, a10, a11 = q.amplitudes()
    print("Amplitudes:")
    print(" |00>:", a00)
    print(" |01>:", a01)
    print(" |10>:", a10)
    print(" |11>:", a11)

    # statistical sampling by copying the state before collapse
    counts = {0:0, 1:0, 2:0, 3:0}
    amps = q.amplitudes()
    for _ in range(1000):
        samp = TwoQubitMatrix(list(amps))
        m = samp.measure_all()
        counts[m] += 1
    print("Counts (|00>,|01>,|10>,|11>):", counts)

By running this program, we realize that despite that our first qubit is in a state of equal superposition after the Hadamard gate (so we have 50% chance that it is at 0 and 50% that it is at 1 when measuring), the measurement of our second qubit is always identical to the measurement of the first qubit. This is because our two qubits are entangled. So, only two output states are possible: |00⟩ and |11⟩.

And here is the equivalent code with Qiskit:

python
from qiskit import QuantumCircuit, transpile
from qiskit_aer import Aer
from qiskit.quantum_info import Statevector

# Circuit: 2 qubits, 2 classical bits
qc = QuantumCircuit(2, 2)
qc.h(0) # Hadamard on qubit 0 (left)
qc.cx(0, 1) # CNOT control 0 (left), target 1 (right)
state = Statevector.from_instruction(qc) # get the statevector after applying gates but before measurement
qc.measure(range(2), range(2)) # measure both qubits and store in classical bits 0 and 1

# We chose that qubit 0 is on the left and qubit 1 is on the right (|q0q1>) to follow the reading order.
# However, Qiskit uses the reverse order (little-endian).
print("Statevector amplitudes (|00>,|01>,|10>,|11>, order |q1q0>):")
print(state.data) # should be ~ [1/sqrt(2), 0, 0, 1/sqrt(2)]

# Simulator
sim = Aer.get_backend("aer_simulator")
tqc = transpile(qc, sim)
result = sim.run(tqc, shots=1000).result()
counts = result.get_counts()

print("Counts (order |q1q0>):", counts)

print(qc.draw()) # display the circuit diagram

When I introduced the Hadamard gate, I referred to it as a quantum “Hello World.” But for many specialists in quantum computing, it is the circuit that I have just presented to you which is the quantum “Hello World”, because it illustrates both superposition and entanglement.

In reality, if you’ve been following my articles since the beginning, you’re probably getting to a point where you’re perplexed and find all of this a bit frustrating. I think that’s normal.

The classic “Hello World” allows you to solve a real-life problem: display a simple message on a computer. Whereas for the moment, we have only been rotating qubits, without solving any concrete problems.

In a future article, I will propose a simple quantum algorithm, which gives a real result for a given problem, and not just probabilities, while reducing the number of iterations needed compared to a classical algorithm.

Don’t miss my upcoming posts — hit the follow button on my LinkedIn profile