Python is the language we will use to calculate numbers, work with equations, and draw graphs throughout this course. You do not need to install it before starting. Google Colab runs Python in your browser and gives you a notebook: a document that keeps your explanation, code, and results together. This lesson takes you from an empty cell to a small, complete physics calculation.
A Jupyter notebook is a file with the ending .ipynb. It contains cells rather than one
long program. Some cells contain text. Others contain Python code. The result of running
a code cell appears underneath it. Colab is a hosted notebook service built on the
Jupyter project. You can download the same notebook and open it in Jupyter on your own
computer later. The physics and Python examples do not depend on a special Colab API.
We will calculate the energy needed to raise a book, change the inputs, and plot how the answer changes. Along the way you will meet variables, functions, arrays, symbolic expressions, and a few useful checks. Each idea starts small. Run one cell and read its output before moving to the next.
Use Open in Google Colab above this lesson. The notebook opens with this lesson's text and Python cells already included. You can edit and run that view without changing the original course notebook. Sign in to your Google account when Colab asks you to connect to a runtime. To keep your work, choose File → Save a copy in Drive, give the copy a name you recognise, and continue there.
The Download notebook link gives you the .ipynb file directly. This is useful if
you prefer a local Jupyter installation, or if you want to upload the file using Colab's
File → Upload notebook menu. Both routes use the same lesson and code. Keep a downloaded
copy if you want a separate record of your work. Your saved file contains the text and
code, and can also include the outputs from your last run.
A runtime is the computer that executes your cells. Your browser displays the notebook, while the Python calculation runs on that computer. A normal CPU runtime is enough for these examples. No graphics processor is needed. If the runtime disconnects, your saved notebook remains, but values held in the runtime's memory may disappear. Running the cells again recreates those values.
Click the triangular run button beside a code cell, or select the cell and press Shift + Enter. This executes the cell and moves to the next one. The first run may take a little longer while Colab connects. Start with this calculation:
print(2 + 3)
print(2 * 3)
print(10 / 4)
print(3**2)
The output is 5, 6, 2.5, and 9, on separate lines. Python uses * for multiplication,
/ for division, and ** for a power. The last line means three squared. Do not use
^ for powers: in Python that symbol performs a different operation on integers.
Parentheses work as they do in ordinary arithmetic. For example, (2 + 3) * 4 evaluates
the addition first.
print asks Python to show a value. You will also see notebooks that put an expression
on the last line without print. Jupyter displays that final expression automatically.
Using print is useful when a cell should show several results or attach a label to a
number. The text inside quotation marks is a string; Python displays it as text rather
than treating it as the name of a variable.
Choose + Text to add a text cell and + Code to add a code cell. In a text cell,
write the question in your own words. You can use Markdown, a simple way to format text.
A line starting with a single # becomes a large heading. Two # symbols make a smaller
heading. Surround a word with two asterisks on each side to make it bold.
Text cells do not run Python. They are where you describe what the calculation means,
where the inputs came from, and which assumptions you made. In a code cell, a # starts
a comment. Python ignores the rest of that line. A comment can explain a working line of
code, but commenting out an entire calculation means the calculation does nothing.
For this notebook, write: “How much does the gravitational potential energy change when I move a book to a higher shelf?” Then record that we use metres for height, kilograms for mass, and an upward positive direction. These choices should remain visible beside the calculation. You should not have to remember them from an earlier conversation.
A variable is a name that refers to a value. The equals sign stores the value on its right under the name on its left. Run this cell:
mass_kg = 2.0
initial_height_m = 0.8
final_height_m = 1.6
gravity_m_per_s2 = 9.81
height_change_m = final_height_m - initial_height_m
print("Height change in metres:", height_change_m)
The height change is 0.8 metres. Names such as mass_kg make the unit visible. The
underscore joins words into one Python name; spaces cannot appear inside a variable
name. Python distinguishes upper and lower case, so mass_kg and Mass_kg would be
different names. Pick one spelling and use it consistently.
A name does not enforce a unit. If you store centimetres under a name ending in _m,
Python will still do the arithmetic. That is why we convert values before calculating
and explain the units in text. Later lessons introduce stronger ways to check units.
For a first notebook, explicit names and a small worked example make mistakes easier
to notice.
Assignment also does not create a permanently linked equation. If you change
final_height_m later, height_change_m keeps its old value until you run the line that
calculates it again. This is one reason execution order matters. After changing an
input, rerun the calculation cells below it.
The system is the book together with Earth. Gravitational potential energy describes their interaction. Near Earth's surface, a small change in height gives
\[\Delta U_g=mg(y_f-y_i)\]The symbol \(\Delta\) means a change: final value minus initial value. Mass is measured in kilograms, gravitational acceleration in metres per second squared, and height in metres. Their product is measured in joules. We use 9.81 as an approximate near-Earth value of gravitational acceleration, not as a measured value for every location on Earth.
energy_change_j = mass_kg * gravity_m_per_s2 * height_change_m
print("Unrounded energy change in joules:", energy_change_j)
print(f"Energy change: {energy_change_j:.1f} J")
The unrounded product is 15.696 joules. The last line displays 15.7 J. An f before a
quoted string lets you insert values inside braces. The :.1f part asks for one digit
after the decimal point. It changes the displayed text, while energy_change_j keeps
its unrounded value for later calculations.
A positive answer means the book-Earth system gained gravitational potential energy. If you lower the book, the height change is negative and the energy change is negative. The calculation does not give the exact energy used by a person's muscles. It describes one energy change in a deliberately simple physical model.
The chosen zero of height does not change this answer. Measuring both shelves from a point one metre below the floor adds one metre to both heights. Their difference stays the same. Measuring one shelf from the floor and the other from the ceiling would mix origins and produce an answer with no useful physical meaning.
A function groups instructions that you want to repeat with different inputs. The
def line names the function and its inputs. The indented lines belong to it. Python
uses indentation to mark this grouping, so keep the four spaces at the start of each
line inside the function.
def energy_change(mass_kg, initial_height_m, final_height_m, gravity=9.81):
if mass_kg < 0:
raise ValueError("Mass must not be negative.")
if gravity <= 0:
raise ValueError("Use a positive gravity magnitude.")
height_change_m = final_height_m - initial_height_m
return mass_kg * gravity * height_change_m
print(f"Raise the book: {energy_change(2.0, 0.8, 1.6):.1f} J")
print(f"Lower the book: {energy_change(2.0, 1.6, 0.8):.1f} J")
print(f"Leave it in place: {energy_change(2.0, 0.8, 0.8):.1f} J")
return sends the calculated value back to the caller. It belongs inside the function.
Defining a function does not run its calculation immediately; the calls in the three
print lines supply the inputs and run it. The default gravity=9.81 means that the
caller can omit that fourth input, or supply a different positive value explicitly.
The two if statements check whether inputs fit our model. Negative mass is excluded,
but negative height change is allowed. The distinction matters: a check should reject
an invalid physical situation, not a perfectly valid downward motion. A ValueError
stops that call and explains what needs to change.
Before making a graph, check simple cases whose answers you can predict. Keeping the
book on the same shelf must give zero. Doubling its mass must double the energy change.
Moving it back must reverse the sign. Use assert to make these expectations executable:
import math
up = energy_change(2.0, 0.8, 1.6)
down = energy_change(2.0, 1.6, 0.8)
double_mass = energy_change(4.0, 0.8, 1.6)
shifted_origin = energy_change(2.0, 1.8, 2.6)
assert energy_change(2.0, 0.8, 0.8) == 0.0
assert math.isclose(down, -up)
assert math.isclose(double_mass, 2 * up)
assert math.isclose(shifted_origin, up)
print("All four physical checks passed.")
import math loads Python's standard mathematics module. math.isclose compares
numbers while allowing a small rounding difference. Many decimal fractions cannot be
stored exactly in a computer's binary number format. A tiny discrepancy after
subtraction is therefore not automatically a physics error.
An assertion that fails raises an error. Read it as a clue: either the calculation or the expectation is wrong. Passing these checks gives useful evidence about the code, but does not prove that the uniform-gravity approximation works at every possible height. Checking implementation and checking physical assumptions are related tasks, and both are needed.
NumPy helps Python work with collections of numbers. Instead of entering five final heights separately, put them in an array. An array is an ordered collection whose arithmetic can act on every element at once.
import numpy as np
final_heights_m = np.array([0.0, 0.4, 0.8, 1.2, 1.6])
energy_changes_j = 2.0 * 9.81 * (final_heights_m - 0.8)
for height, energy in zip(final_heights_m, energy_changes_j):
print(f"Final height {height:.1f} m -> change {energy:.3f} J")
Subtracting 0.8 subtracts it from every height. Multiplication then applies to each height change. The output array has the same number of entries and the same order. The first energy is negative, the middle energy is zero, and the last is positive. These signs describe shelves below, level with, and above the initial shelf.
The for loop prints one pair at a time. zip pairs corresponding entries in the two
arrays. The calculation itself already happened before the loop. Keeping calculation
and display separate lets you reuse the numerical values in a table, a graph, or a
later calculation without reading numbers back from formatted text.
Matplotlib turns numerical results into figures. NumPy's linspace gives us 101 evenly
spaced heights from zero to two metres, including both endpoints. This makes a smooth
looking line without typing every height ourselves.
import matplotlib.pyplot as plt
heights_m = np.linspace(0.0, 2.0, 101)
energies_j = 2.0 * 9.81 * (heights_m - 0.8)
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(heights_m, energies_j, label="2 kg book")
ax.axhline(0, color="grey", linewidth=0.8)
ax.axvline(0.8, color="grey", linewidth=0.8, linestyle="--")
ax.set_xlabel("Final height (m)")
ax.set_ylabel("Potential energy change (J)")
ax.set_title("Moving a book from a shelf at 0.8 m")
ax.legend()
ax.grid(alpha=0.25)
fig.tight_layout()
plt.show()
fig is the whole figure and ax is the plotting area with its axes. ax.plot receives
horizontal values first and vertical values second. The axis labels say which
quantities are shown and give their units. plt.show() displays the completed figure
under the cell.
Read the graph before changing it. The line crosses zero at 0.8 metres because that is the starting shelf. Its positive slope says that a higher final shelf gives a larger energy change. The slope is 19.62 joules per metre, equal to mass times gravity. A straight line is expected because the model uses a constant gravitational field.
More plotted points make the line look smoother. They do not make the input mass more accurate or improve the gravity approximation. This is a useful distinction to keep throughout the course: the detail in a picture and the accuracy of a physical model are different things.
NumPy calculates with numerical values. SymPy can keep quantities symbolic and rearrange an equation before we choose values. Start by declaring symbols. These are mathematical objects, not measurements already stored in kilograms or metres.
import sympy as sp
m, g = sp.symbols("m g", positive=True)
y_i, y_f = sp.symbols("y_i y_f", real=True)
delta_u = m * g * (y_f - y_i)
print("Energy expression:", delta_u)
print("Slope with respect to final height:", sp.diff(delta_u, y_f))
value = delta_u.subs({m: 2, g: 9.81, y_i: 0.8, y_f: 1.6})
print(f"Numerical energy change: {float(value):.3f} J")
sp.diff differentiates the expression. Its result is g*m, which matches the slope we
read from the graph. subs substitutes values into the symbolic expression. The call
to float converts the resulting SymPy number to an ordinary Python floating-point
value so that the familiar formatting works.
SymPy can also answer the reverse question. Suppose the energy increase is 20 joules. At what height should the final shelf be, under the same assumptions?
target_energy = sp.Symbol("target_energy", real=True)
height_solution = sp.solve(sp.Eq(delta_u, target_energy), y_f)[0]
print("Final height formula:", height_solution)
height_value = height_solution.subs(
{
m: 2,
g: 9.81,
y_i: 0.8,
target_energy: 20,
}
)
print(f"Final shelf height: {float(height_value):.3f} m")
The result is approximately 1.819 metres. sp.Eq states a mathematical equality and
sp.solve asks for the specified unknown. The [0] selects the first solution from
the returned list; this particular equation has one solution for positive mass and
gravity. Other physics equations can have several solutions, and you must decide which
ones fit the physical question.
Try a one-kilogram book while keeping the two shelves fixed. Predict the answer first: the energy change should be half of 15.696 joules. Then run the cell. Next keep the mass at two kilograms but move the final shelf down to 0.4 metres. Predict the sign before calculating the value.
cases = [
("Lighter book", 1.0, 0.8, 1.6),
("Lower shelf", 2.0, 0.8, 0.4),
("Same shelf", 2.0, 0.8, 0.8),
]
for label, mass, start, finish in cases:
result = energy_change(mass, start, finish)
print(f"{label}: {result:.3f} J")
The results are 7.848 J, −7.848 J, and 0 J. Write a text cell below the outputs saying why each answer has that size and sign. This is more useful than saving a notebook full of unexplained numbers. It also gives you a way to catch a mistake that produces a plausible looking number with the wrong interpretation.
To compare masses on one graph, reuse the same height array for every curve. Keep the starting shelf fixed, so that mass is the only changing input. The shared zero crossing then tells you something that a table of unrelated calculations might hide.
fig, ax = plt.subplots(figsize=(7, 4))
for mass in [1.0, 2.0, 3.0]:
changes = mass * 9.81 * (heights_m - 0.8)
ax.plot(heights_m, changes, label=f"{mass:.0f} kg")
ax.set_xlabel("Final height (m)")
ax.set_ylabel("Potential energy change (J)")
ax.legend(title="Book mass")
ax.grid(alpha=0.25)
fig.tight_layout()
plt.show()
Every curve passes through zero at the starting height. Heavier books have steeper curves, but the sign still depends on whether the final shelf is above or below the starting shelf. Try stating that conclusion without referring to line colours. A good explanation should remain understandable when the figure is printed in black and white.
A NameError often means that a name was misspelled or that the cell defining it has
not run. If np is unknown, run the import numpy as np cell. If heights_m is unknown,
run the cell that creates the height array. Start at the earliest missing definition
rather than adding a new value in a random cell near the error.
A SyntaxError means Python could not read the code. Check for missing parentheses,
unclosed quotes, and a missing colon after def, if, or for. An IndentationError
points to the spacing that groups statements. The error message usually includes a
line number and shows the location where Python became unable to continue.
A ModuleNotFoundError means a required library is absent from the runtime. The course
notebooks include a setup cell for NumPy, SymPy, and Matplotlib. Run that cell first in a
fresh environment. If you are using your own Jupyter installation, select the Python
environment where those libraries are installed. Installing a package in a different
Python environment will not make it appear in the current notebook.
You do not need to erase the whole notebook after an error. Correct the relevant cell, run it again, and rerun the cells that depend on its result. Keep a short note if the error revealed a misunderstanding about the physics. That note may be more valuable than the final decimal answer.
Before sharing, restart the runtime and run all cells from the top. In Colab, use the restart-and-run-all option in the Runtime menu. This removes values left over from experiments and shows whether the saved cell order really reproduces the calculation. The setup and import cells should run before any cell that needs their libraries.
Save the notebook after reviewing the fresh outputs. Add your name or a short purpose statement, label the values as illustrative rather than measured, and record the assumptions: a small lift near Earth's surface, uniform gravity, and both heights measured from the same origin. If you later replace the sample inputs with measurements, record how you measured them and what their uncertainties mean.
The finished notebook now contains a question, inputs, a working Python calculation, checks, graphs, and an explanation. You can change one value and follow its effect all the way to the conclusion. That is the basic workflow we will reuse when the equations become more involved: keep the reasoning visible, calculate with code, and use the result to answer a physical question.