Topic 16.2
Jupyter Notebooks and the Experiment Workflow
In one line
A notebook is a document of code cells that share one running Python process, which makes it excellent for exploring data and risky for anything you need to repeat. The workflow is to explore in a notebook, then move the working code into modules with tests, seeds and locked dependencies.
Think of it like this
A lab notebook on a workbench. You try a mixture, jot the result next to it, try another. It is perfect for discovering things. But if you want someone else to make the same mixture next month, you do not hand them the messy notebook. You write a clean recipe. A Jupyter notebook is the lab notebook; a Python module is the recipe.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Notebook
- A document made of code cells and text cells whose outputs are saved inside it.
- Cell
- One block of code or text in a notebook. You run it on its own.
- Kernel
- The running Python process behind a notebook. Its memory is shared by every cell.
- Hidden state
- Values still alive in the kernel that no visible cell order would create.
- Seed
- A starting number for a random number generator. The same seed gives the same "random" numbers.
- Reproducible
- Another run gets the same result.
- Lock file
- A file listing the exact version of every dependency, so installs match.
Step by step
01A notebook is a shared dictionary
Under the hood, every cell runs in the same namespace, like a single dictionary of variables. The first example below builds a toy kernel so you can see it. It uses the built-in exec, which runs a piece of text as Python code. You will rarely need exec yourself. Here it stands in for "run this cell". Running a cell never clears what earlier cells left behind.
02The hidden-state trap
Suppose cell 1 sets price = 100, cell 2 prints it, and cell 3 computes a discount. You change cell 1 to 500 and re-run only cell 1. Cell 2's old output on screen still says 100 until you run it. Worse, if you delete cell 1 entirely, the kernel still remembers price, so everything keeps working until the day you restart.
03Restart and run all
The fix is mechanical: throw the memory away and run every cell in order. In a real notebook this is a menu item. In the toy kernel it is creating a new Kernel() and replaying the cells. If the result differs, your notebook depended on hidden state.
def run_in_order(cells):
fresh = Kernel() # "Restart kernel and run all"
for code in cells:
fresh.run_cell(code)
return fresh.state04Moving code into a module
When a cell's code works, cut it into a function in a .py file. Then the notebook says from experiment import noisy_scores. Now the logic can be imported, tested with pytest (Phase 11) and reviewed in a normal diff. The second example writes a module and imports it, exactly as you would from a notebook.
05Seeds and pinned versions
Random numbers in Python come from a generator that starts at a seed. Give it the same seed and you get the same stream. Recall from Topic 14.4: NumPy's modern way is np.random.default_rng(seed). The standard library's is random.Random(seed). Pass the seed in as a parameter instead of setting a hidden global, so the notebook cell and the tests use the same one.
For dependencies, write exact versions in a lock file (uv.lock, or a pinned requirements.txt). A notebook that worked last spring and fails today is very often a library that updated.
numpy==2.2.0
pandas==2.3.006Notebooks in the AI workflow
With LLMs, a notebook is a good place to try a prompt (the text you send to the model) on ten examples and read the answers. Once a prompt works, move it into a template (Topic 16.7), the call into a function (Topic 16.3), and add tests with a fake model (Topic 16.11). Keep API keys out of notebooks: read them from environment variables, because notebooks get shared, committed and screenshotted.
Try it yourself
- 1
Break the order
In the first example, swap two of the
run_celllines so a cell uses a name before it exists. Predict the error first, then run. - 2
Prove reproducibility
In the second example change the seed from 7 to 8 in the first call and compare. Then put it back and confirm run 1 and run 2 match.
Code & diagrams
Read the comments, predict each printed line, then run it.
Expected output
cell 2: price is 100
cell 2 again: 500 -> discounted 450.0
discounted is still 450.0 but price is 50Same seed, same numbers. A different seed gives different numbers.
Expected output
run 1: [70.006, 71.494, 68.629, 65.547]
run 2: [70.006, 71.494, 68.629, 65.547]
other seed: [61.309, 63.317, 63.194, 68.242]
shuffled: ['d', 'a', 'c', 'b']Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Run a cell before its dependency
Use a name that an earlier cell was supposed to define, but that was never run.
data = [3, 5, 8]
print("average:", sum(data) / len(data))
print("largest:", biggest)Myth vs fact
Myth
A notebook that shows the right output is correct.
Fact
The output may be left over from an earlier run or from a deleted cell. Only a clean restart and run-all proves it.
Myth
Notebooks are only for beginners.
Fact
Researchers and senior engineers use them daily for exploration. The skill is knowing when to leave the notebook and move code into tested modules.
Myth
Setting a seed makes everything reproducible.
Fact
A seed covers one random generator. Library versions, data changes, hardware differences (GPU arithmetic) and hosted model updates can still change results.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A
.ipynbfile is JSON that stores code, outputs and metadata. Outputs and execution counters create noisy diffs. Teams strip outputs before committing (with tools such as nbstripout) or pair notebooks with plain Python files, so code review stays readable. - ▸
Tools that run notebooks as tests exist (executing every cell in a clean kernel in CI), which turns "restart and run all" into an automatic check. Pair it with a pinned lock file so CI and your machine agree.
- ▸
Seeds interact with parallelism. Splitting work across processes needs separate child seeds (NumPy's
SeedSequenceis built for that), otherwise workers may repeat the same stream.
Remember this
- 1
A notebook (a
.ipynbfile) is a list of cells: some hold code, some hold text. A kernel is the Python process that runs the code cells. All cells share the kernel's memory, so a variable defined in one cell is visible in the next. The output of a cell (text, tables, charts) is saved right under it. - 2
The central trap is hidden state. You can run cells in any order, edit a cell and not re-run the ones below it, or delete a cell whose variable still lives in the kernel. The notebook on screen then shows results that no top-to-bottom run would produce. Two people with the same file can get different answers.
- 3
The cure is a habit: Restart the kernel and run all cells before you trust or share a result. If it fails or the numbers change, the notebook was lying. Also keep cells small, give them clear order, and put real logic in functions.
- 4
Notebooks shine for exploring a dataset, drawing quick charts, trying a prompt and looking at the output, and teaching. They are weak for code that must be tested, imported by other code, reviewed line by line in version control (the saved JSON is noisy), or run on a schedule.
- 5
The workflow professionals use: explore in a notebook, extract every function that works into a
.pymodule, import it back into the notebook, and test the module. The notebook becomes a thin report on top of tested code. VS Code notebooks and Google Colab are two common places to run notebooks; Colab also gives access to a GPU. - 6
Reproducibility means someone else (or you in six months) gets the same result. Three habits give most of it: set seeds for every random number generator, pin dependency versions in a lock file (Phase 6), and record the data version you used. AI work adds one more. A hosted model is a model that runs on a company's servers, and you call it over the internet. The company can update it, so log which model name you called.
Explain it without notes
What is a kernel, and why does it cause the hidden-state problem?
How do you make a notebook result trustworthy and shareable?
Describe a workflow that takes an experiment from a notebook to production code.
Two data scientists run the same notebook and get different numbers. List the causes you would check, in order.
Practice
Write a function run_in_order(cells) that runs a list of code strings in a fresh shared namespace and returns the namespace.
Write a function sample(seed) that returns five dice rolls from its own seeded generator, and show that equal seeds match.
Write a function to_requirements(reqs) that turns a dict of name to version into sorted pinned lines.
Trade-offs
- ↔
Notebooks trade repeatability for speed of exploration. You see results instantly and keep the narrative with the code. The price is hidden state and poor fit for testing and review.
- ↔
Moving code out costs time now and saves it later. Extracting modules feels slower on day one. It is what makes the work testable, reusable and safe to hand to a teammate.
Done when you can
Done when you can explain a kernel and cells in your own words.
Done when you can give an example of hidden state and fix it with restart and run-all.
Done when you can seed both
randomand NumPy generators and show two runs match.Done when you can move a working cell into a module and import it.
Done when you know why API keys do not belong inside notebooks.