Learning Quantum Computing with Python - Part 2
October 29, 2025
Introduction
This is a continuation from part 1 of my introduction to the qubit world. As a review, I am following a text book on quantum computing called Building Quantum Software with Python: A Developer's Guide. In part 1, I covered chapters 1 and 2. In this post, I will talk through my work on chapter 3. In chapter 3, we covered single qubit quantum states and were introduced to quantum gates.
Chapter 3 review
The main task of this chapter was to create our first quantum simulator. This essentially means we simulate quantum systems, measurements, and gates on a classical computer where we use arrays to store our probabilities for certain outcomes. The probabilities are calculated based on amplitudes, direction, and magnitude. Then, by applying certain gates, we are able to manipulate the quantum system to change the probabilities of certain outcomes. One of the key points here is that each gate will always affect a pair of quantum probabilities.
NOT (X) gate
For example, the NOT gate (or X gate) swaps the amplitudes of 2 outcomes. This is the simplest gate as to implement where the gate is defined as x = [[0, 1], [1, 0]]. And so, when we call the transform method and pass the x gate as the parameter, we see the amplitudes get switched.
# This was provided by the text book
def transform(state, gate):
assert(len(state) == 2)
z0 = state[0]
z1 = state[1]
state[0] = gate[0][0]*z0 + gate[0][1]*z1
state[1] = gate[1][0]*z0 + gate[1][1]*z1
print("NOT gate")
print(f"Starting state {state}")
transform(state, x)
print(f"Transformed state {state}")
# NOT gate
# Starting state [0.5000000000000001, 0.8660254037844386]
# Transformed state [0.8660254037844386, 0.5000000000000001]The other gates apply similar, yet different transformations to our quantum states.
Z gate
The Z gate mutliplies one side of the pair of amplitudes by -1 which flips the sign of both the real and imaginary parts of the amplitude. When we use the same tranform method with the z gate, we see the signs flip for the second outcome.
state = init_state()
state = [state[1], state[0]]
print(state)
transform(state, z)
print("Z transform")
print(state)
# starting state: [0, 1]
# Perform Z transform
# State after transform [0, -1]Phase gate
The phase gate rotates one side of the amplitudes by a certain angle denoted by P(ϕ) where ϕ is the rotation angle (in radians). When a state is passed through this gate, the phase angle is added to the direction of the outcome. θ direction becomes ϕ + θ. One thing to note about this gate is that the final probabilities do not change.
From chapter 1:
Each such probability is the squared length of a planar vector. Richard Feynman informally calls these vectors "arrows." Formally, they are called amplitudes, and mathematically, they are best described as complex numbers. The direction of amplitudes is essential in how the state of quantum systems changes but not in the frequency of the computation outcomes they are associated with.
In practice, we can use the cis function from the provided utils.py to run:
phi = pi/3
state = [state[0], cis(phi)*state[1]]
# Output
#[1, 0j]Hadamard (H) gate
The H gate is named after Jacque Hadamard, a famous mathematician known for the hadamard 2x2 matrix. This gate applies to one probability side the sum of the pair of amplitudes divided by the square root of 2 and the other side the difference of the pair of amplitudes divided by the square root of 2. From furthere reading, the H gate is supposedly what gives a qubit the superposition attribute and applying it again returns the qubit to its original state. The following code shows the application of the H gate to a single qubit state: state = [sqrt(0.5)*(state[0] + state[1]), sqrt(0.5)*(state[0] -state[1])]. The book does not go much more in-depth on the H gate, so I am assuming the effects will be shown at deeper levels in later chapters.
Rz gate
When applied, the Rz gate rotates the "0" side of an amplitude by θ/2 clockwise and the "1" side by θ/2 counter-clockwise. Like the phase gate, this gate changes the direction of an amplitude but does not affect the probability of the outcome and is applied with
theta = pi/3
state = [cis(-theta/2)*state[0], cis(theta/2)*state[1]]Y gate
Y gate rotates each side of amplitudes (0 and 1) by 90 degrees with one being clockwise and the other counter-clockwise, respectively, then swaps them. The effect is that the direction is changed and the probabilities are switched. The gate can be applied with y = [[0, complex(0, -1)], [complex(0, 1), 0]].
Rx and Ry gates
These gates were difficult to understand, but the book's provided code shows these gates apply rotations to impact direction, magnitude, and amplitudes.
def rx(theta):
return [[cos(theta/2), complex(0, -sin(theta/2))], [complex(0, -sin(theta/2)), cos(theta/2)]]
def ry(theta):
return [[cos(theta/2), -sin(theta/2)], [sin(theta/2), cos(theta/2)]]Quantum circuits and measurements
To me, this was the best part of the chapter because I was actually able to see the results of applying these gates. Applying a series of gates is called a quantum circuit. For example, the following circuit HP(π/3) XRY(2π/3) is executed from right to left:
s = init_state()
transform(s, ry(2*pi/3))
transform(s, x)
transform(s, phase(pi/3))
transform(s, h)
# Generates the following output:
# Quantum Circuit HP(π/3) XRY(2π/3) ---
# Initial state:
# [0, 1, 0.0, 1, 1]
# [1, 0, 0.0, 0, 0]
# Ry gate:
# [0, 0.5, 0.0, 0.5, 0.25]
# [1, 0.86603, 0.0, 0.86603, 0.75]
# X gate:
# [0, 0.86603, 0.0, 0.86603, 0.75]
# [1, 0.5, 0.0, 0.5, 0.25]
# phase gate:
# [0, 0.86603, 0.0, 0.86603, 0.75]
# [1, 0.43301j, 60.0, 0.5, 0.25]
# H gate and final state:
# [0, (1+0.30619j), 21.20602, 0.84647, 0.71651]
# [1, -0.30619j, -35.10391, 0.53244, 0.28349]From this final state, we can run measurements on a quantum state using choices from the random python package. This will generate a sample of data of k number of measurements. Our final states' probabilities after the applied circuit were approximately 71% for 0 and 29% for 1. So if we run 10 measurements, we should expect to see a similar distribution. The quantum measurement is non-deterministic which means each run we will get a different output.
# 10 measurements
samples = choices(range(len(s)), [abs(s[k])**2 for k in range(len(s))], k=10)
print(samples)
for (k, v) in Counter(samples).items():
print(str(k) + ' -> ' + str(v))
# Generates the following output
# [0, 0, 0, 1, 0, 1, 0, 0, 0, 0]
# 0 -> 8
# 1 -> 2
# 1000 measurements
samples = choices(range(len(s)), [abs(s[k])**2 for k in range(len(s))], k=1000)
for (k, v) in Counter(samples).items():
print(str(k) + ' -> ' + str(v))
# Produces output
# 0 -> 739
# 1 -> 261The final part of this chapter applies quantum circuits in order to get a certain probability of distribution. The book uses Bernoulli distribution as an example. If we want the probabilty of outcome 0 to be p = 0.7 (which means outcome 1 will be p=1-0.7), we would first find θ using theta = 2*acos(sqrt(p)) then applying the Ry gate Ry(theta) to our initial state.
p = 0.7
theta = 2*acos(sqrt(p))
s = init_state()
transform(s, ry(theta))
print_state(s)
# Generates output
# [0, 0.83666, 0.0, 0.83666, 0.7]
# [1, 0.54772, 0.0, 0.54772, 0.3]Key takeaways
This chapter gave me my first taste in actually creating a quantum system using quantum circuits and simulating a series of measurements. The actual mathematics behind the gates were confusing and a bit beyond my current skill level, but getting to visually see the changing state and how the probabilities get shifted according to certain inputs was practical and gets me excited for the following chapters. Chapter 4 will explore circuits with additional qubits.