2  Primer P01 - Running Python Experiments

2.1 Learning objectives

After completing this compulsory primer, you will be able to:

  • check that the Python interpreter in use belongs to the intended local environment and run commands from a terminal;
  • write expressions, bind values to names, and recognize int, float, str, bool, and None;
  • use operators, call functions, import modules, and find help;
  • distinguish Jupyter cells, Quarto code blocks, and Python scripts;
  • read the important parts of a traceback without guessing;
  • distinguish a computed value from the text or display presented to the reader; and
  • uncover hidden notebook state by restarting the kernel and running all cells from the beginning.

This primer assumes no previous programming experience. You need only basic arithmetic. All accompanying code for this primer is original, runs locally with the Python standard library, and is located in source/code/primer01_execution.py.

2.2 The local runtime and terminal

The runtime is the program that actually executes the code. In this unit, that runtime is a local Python interpreter. A terminal is a text window in which we ask the system to run programs. Jupyter and Quarto use the same runtime when both are configured to use the same environment.

From the project root, check the runtime with the following commands. Type the commands themselves, not any prompt symbol displayed to their left.

python --version
python -c "print(2 + 3)"

The first command should report Python 3. The second asks Python to calculate 2 + 3 and then write 5 to the terminal. If python is not found, do not install packages at random. First follow the local setup guide, activate the course environment, and check the active version again.

Three preliminary questions prevent many irreproducible results:

  1. What command was run?
  2. From which working directory was it run?
  3. Which runtime and environment received the command?

A relative path such as source/code/primer01_execution.py is interpreted relative to the working directory. For this reason, the examples in this book are run from the project root.

2.3 Expressions, names, and assignment

An expression asks Python to produce a value. The number 4, the expression 2 + 3, and the call len("data") each produce a value. The = operator performs assignment: Python evaluates the right-hand side and then binds the result to the name on the left-hand side.

length = 8
width = 5
area = length * width
area
Listing 2.1
40

The name area is not a permanent box. A later assignment can bind the same name to a new value. To check whether values are equal, use ==, not =.

area == 40
Listing 2.2
True

Choose meaningful names, such as sample_count, rather than vague names such as x1, unless that short symbol is itself part of the mathematical model.

2.4 Your first five kinds of scalar values

Common scalar values you will encounter first are:

Type Example Initial meaning
int 8 an integer
float 0.01 an approximation to a real number
str "test A" text
bool True a truth value: true or false
NoneType None no value yet, or no result
count = 8
tolerance = 0.01
label = "test A"
passed = count > 5
note = None

type(count).__name__
Listing 2.3
'int'

Call type(...) with the other names to check their types one at a time. Lists, tuples, loops, and comprehensions are deliberately left until P02; this primer does not need them yet.

None is different from 0, False, and the empty string "". It indicates that no value has been supplied. Do not use None as a number.

The basic arithmetic operators are +, -, *, /, //, %, and **. Division with / produces a float; // is floor division; % gives the remainder consistent with floor division; and ** is exponentiation.

sum_result = 7 + 2
difference = 7 - 2
product = 7 * 2
quotient = 7 / 2
floor_quotient = 7 // 2
remainder = 7 % 2
power = 7**2

power
Listing 2.4
49

All seven names still hold scalar values. No collection has been created; lists and tuples are introduced in P02.

The comparisons ==, !=, <, <=, >, and >= produce bool values. The operators and, or, and not combine truth values. Use parentheses when the order of operations needs to be clear to the reader.

2.5 Function calls, imports, and help

A function accepts arguments and may return a value. In round(3.14159, ndigits=3), 3.14159 is a positional argument and ndigits=3 is a keyword argument.

round(3.14159, ndigits=3)
Listing 2.5
3.142

A module groups reusable names. Import the math module, then include the module name so that the function’s origin remains visible.

import math

radius = 3
circle_area_value = math.pi * radius**2
circle_area_value
Listing 2.6
28.274333882308138

Use help(math.sqrt) in Python or math.sqrt? in a Jupyter cell to view local documentation. Help explains a function’s contract; it does not guarantee that the function is suitable for your question. Check the meaning of the arguments, units, domain, return value, and possible errors.

2.6 Jupyter cells and Quarto blocks

A Jupyter notebook has code cells and Markdown cells. Code cells are sent to the Python kernel; Markdown cells contain explanations, equations, and the connections between questions and results. Quarto uses fenced code blocks in .qmd files and can execute them when building HTML or PDF.

The source of a Quarto code block looks like this:

~~~{python}
value = 6 * 7
value
~~~

Running one cell does not automatically run all the cells above it. The kernel retains names while its process is alive. Cell execution numbers show the order in which cells actually ran; a cell’s position on the page shows only the source order. This distinction matters for reproducibility.

2.7 Computation is not display

Python can compute a value without displaying it. In a notebook, the value of the last expression is usually passed to the display mechanism. Assignment usually does not display a representation of the value. print(...) writes text to standard output. In an ordinary script, a bare expression such as 2 + 3 is still evaluated but is not automatically printed.

Listing 2.7
computed_value = 2 + 3
display_text = f"2 + 3 = {computed_value}"
print(display_text)
2 + 3 = 5

computed_value is an int with value 5. display_text is a str chosen for communication. An attractive display does not prove that a computation is correct, and a value in memory is not yet a saved artifact. If a result needs to be audited, save the data and metadata in specified files.

Quarto can capture cell output for inclusion in the reader. That output still needs interpretation: is it a value, diagnostic text, a figure, or an error message?

2.8 Reading a traceback

When Python fails, it displays a traceback. Read the last line first: it gives the error type and a short message. Then find the nearest frame that points to your file and line of code. The frames above it explain the chain of calls that brought execution there.

Traceback (most recent call last):
  File "experiment.py", line 2, in <module>
    result = data * 2
NameError: name 'data' is not defined

NameError means that a name has not been bound in the namespace being used. TypeError usually means that an operation received an unsuitable type of value. ZeroDivisionError indicates division by zero. SyntaxError means that the source could not be parsed as a Python program. The message is initial evidence, not a reason to delete lines at random.

In P01, run a failing example in a console or practice cell, read the traceback, and then correct its cause. The try/except syntax is taught in P02; the book’s supplied checking blocks may use it as scaffolding, but you are not yet expected to write that structure yourself. Do not hide failures throughout the program merely to make the output look green.

2.9 Your first script and local record

A .py file is script source that can be run again. From the project root, run the P01 companion:

python source/code/primer01_execution.py --output output/p01-results.json

The script computes the same example each time and writes a JSON record. It also prints one location message so that a person can tell which file was created. Open output/p01-results.json as text. The computed_value field stores the computed value, while display_text stores the text chosen for the reader. The runtime fields name the Python implementation and version that actually ran the script.

For now, simply understand that the terminal runs the file from beginning to end. Module structure and the main part of a script are discussed in P02.

2.10 Hidden state and restart-and-run-all

Consider the following two cells.

# Cell A
data = 7

# Cell B
result = data * 2

If Cell A has run before, Cell B may succeed even after Cell A is moved, changed, or deleted from the notebook. The old value of data remains alive in the kernel. The notebook appears correct, but the visible source is not sufficient to produce that result. This is hidden state.

The compulsory check before accepting a notebook is to:

  1. save the source;
  2. select Restart Kernel and Run All Cells;
  3. run the cells in source order from a clean namespace;
  4. make sure no unrecorded manual input is required; and
  5. compare the generated artifacts with the expected results.

The companion notebook provides three state checks that can be run directly from the Jupyter interface. The required test is to run the last cell with old state, restart the kernel, show that the cell fails on its own, and then select Restart Kernel and Run All Cells to show that the complete source sequence succeeds. The code companion also provides run_cells for the test suite; tuples, dictionaries, and exception handling within that scaffolding are discussed in P02. The notebook deliberately retains the cross-edition fixture name hasil (Indonesian for result) so the hidden-state sequence and its versioned receipt remain directly comparable with the source edition.

A successful restart-and-run-all demonstrates that the recorded cell sequence can execute in the current environment. It does not yet prove that the mathematical model is appropriate or that another environment will produce identical bytes.

The same exercise is available in the P01 clean-kernel notebook.

The notebook runs locally and contains all three checks in complete source order; its kernelspec metadata points to the project’s frozen o002-frozen environment.

2.11 Exercises

2.11.1 Exercise 1 - values, types, and operators

Create five names: count=8, tolerance=0.01, label="test A", a boolean passed that checks whether count > 5, and note=None. Also calculate the floor quotient and remainder when dividing count by 3. Run the code.

Use // for the floor quotient, % for the remainder, and type(...) to check the types of the values.

ImportantSelf-check

Add the following checks below your code. The cell should finish without an AssertionError.

assert type(count) is int
assert type(tolerance) is float
assert type(label) is str
assert type(passed) is bool and passed is True
assert note is None
assert count // 3 == 2
assert count % 3 == 2

Solution 2.1.

count = 8
tolerance = 0.01
label = "test A"
passed = count > 5
note = None
floor_quotient = count // 3
remainder = count % 3

assert floor_quotient == 2
assert remainder == 2

Each name is bound to the value produced by its right-hand side. The comparison count > 5 produces a bool; None is not the number zero.

2.11.2 Exercise 2 - imports, calls, and help

Import the math module, then calculate the area of a circle of radius 3 using circle_area from the P01 companion. Use local help to find the meaning of math.isclose, then compare the function result with math.pi * 3**2.

Import the companion with from source.code.primer01_execution import circle_area. Call help(math.isclose) in a separate console if the output is long.

ImportantSelf-check
import math
from source.code.primer01_execution import circle_area

exercise_area = circle_area(3)
assert math.isclose(exercise_area, math.pi * 9, rel_tol=0.0, abs_tol=1e-15)

Solution 2.2.

import math
from source.code.primer01_execution import circle_area

exercise_radius = 3
exercise_area = circle_area(exercise_radius)
assert math.isclose(
    exercise_area,
    math.pi * exercise_radius**2,
    rel_tol=0.0,
    abs_tol=1e-15,
)

The import provides a module or function name. The call supplies the argument 3 and returns a value. The isclose documentation explains the relative and absolute tolerances that form part of the check.

2.11.3 Exercise 3 - reading and fixing a traceback

Call circle_area(-2), read the traceback, and state the error type and its final message. Then correct the input to 2 without changing the function merely to hide the error.

The last line names ValueError. Run the failure in a separate console. The following check is supplied scaffolding; its try/except syntax will be covered in P02.

ImportantSelf-check
from source.code.primer01_execution import circle_area

try:
    circle_area(-2)
except ValueError as radius_error:
    assert str(radius_error) == "radius must not be negative"
else:
    raise AssertionError("negative input should have been rejected")

assert circle_area(2) > 0

The type is ValueError and the message is radius must not be negative. The function rejects input outside its contract. The correct solution is to supply a nonnegative radius or correct the data source, not to remove the check.

Solution 2.3.

from source.code.primer01_execution import circle_area

valid_radius = 2
valid_area = circle_area(valid_radius)
assert valid_area > 0

2.11.4 Exercise 4 - computation, display, and scripts

Run the P01 script from the terminal. In Python, call computation_and_display(8, 5). Identify the field that stores the computed result and the field that stores only display text. Explain why printing text is not the same as saving a JSON record.

The computed value has type int; the display has type str. print sends text to standard output, whereas the script writes bytes to the specified path. Dictionary indexing in the checking block is supplied scaffolding and will be explained in P02.

ImportantSelf-check
from source.code.primer01_execution import computation_and_display

display_record = computation_and_display(8, 5)
assert display_record["computed_value"] == 13
assert display_record["display_text"] == "8 + 5 = 13"
assert type(display_record["computed_value"]) is int
assert type(display_record["display_text"]) is str

Solution 2.4.

from source.code.primer01_execution import computation_and_display

display_record = computation_and_display(8, 5)
value_computed = display_record["computed_value"]
text_displayed = display_record["display_text"]
assert value_computed == 13
assert text_displayed.endswith("= 13")

Terminal text can disappear when the terminal closes, and it does not bind the result to a runtime version or parameters. The script’s JSON file is an artifact that can be read again, checked, and hashed. Both still need to be evaluated against the actual mathematical question.

2.11.5 Exercise 5 - clean kernels and hidden state

Use the P01 notebook to demonstrate three facts: Cell B succeeds in an old kernel that has run Cell A; Cell B fails with NameError in a clean kernel; and the sequence A followed by B succeeds after Restart Kernel and Run All Cells. This is a compulsory mastery exercise.

Open source/notebooks/o002-p01-clean-kernel.ipynb. Follow the three marked states in the notebook and do not add unrecorded manual values. The following checking block is project-test scaffolding that you only need to run in P01; the lists, Path, attributes, keyword arguments, subprocess, and sys it uses are discussed in P02 or later project units.

ImportantSelf-check
from pathlib import Path
import subprocess
import sys

kernel_test = subprocess.run(
    [
        sys.executable,
        "-m",
        "unittest",
        "tests.test_primer01.Primer01NotebookKernelTests",
    ],
    cwd=Path.cwd(),
    capture_output=True,
    text=True,
)
assert kernel_test.returncode == 0, kernel_test.stdout + kernel_test.stderr
assert "OK" in kernel_test.stderr
notebook_check_result = "actual kernel: fails out of order; clean run-all passes"
notebook_check_result

Solution 2.5.

'actual kernel: fails out of order; clean run-all passes'

The test starts a real Jupyter kernel. Cell B alone fails with NameError, whereas the complete notebook runs Cell A followed by Cell B in a new kernel and produces 14. The source order, not an old history of clicks, is the evidence that can be executed again.

2.12 Summary

  • A terminal runs commands with a particular runtime and working directory.
  • An expression produces a value; assignment binds that value to a name.
  • int, float, str, bool, and None have different roles.
  • Functions are called with arguments; modules are imported; local help explains contracts that need to be checked.
  • Notebooks and Quarto execute cells, whereas a script executes a file from the beginning. Execution order is part of the experiment.
  • A computed value, display text, and a saved artifact are not the same thing.
  • A traceback gives the location, type, and message of an error; read it before fixing the code.
  • Restart Kernel and Run All Cells detects hidden state, but does not replace mathematical checks or a frozen environment.