A notebook is the right tool for looking at what a model does to your data, and the wrong tool for everything after that.
Three additions — a spend guard, a disk cache and an escape route into a module — keep the first without acquiring the second.
What actually goes wrong Not vague “notebooks are bad practice” complaints.
Four specific failures, each with a specific remedy below.
Money leaves without a number attached.
Re-running a cell that loops over a dataframe is a full re-run of the job.
Nobody budgets for exploration, so nobody notices until the invoice.
The state is invisible.
Out-of-order execution means the variable in memory may have been produced by a cell that no longer exists.
A notebook that runs top-to-bottom in a fresh kernel is the only kind that means anything.
The prompt is stuck in the notebook.
The version that worked is in cell 34 of an untracked file, and the service uses a different one that somebody retyped.
Outputs get committed.
Keys, customer data and fifteen megabytes of base64 images, in a file whose diff nobody reads.
A spend guard in the first cell A counter that raises when the notebook has spent more than you decided it could.
This is the single highest-value cell in an LLM notebook, because it converts an unbounded mistake into a stack trace.
Call after every request and put at the end of any cell that loops.
The raise is deliberately unhandled: a warning in a notebook scrolls past, an exception stops the loop.
Two extra habits belong with it.
Test every loop on first, always, and make the sample size a variable at the top rather than an edit inside the loop.
And put an explicit on every call — the default on some endpoints is the model’s maximum, which is how a five-cent experiment becomes five euros.
Cached cells The reason notebooks cost money is that you re-run cells constantly, and nine times in ten the inputs have not changed.
A disk cache keyed on the request makes the tenth run free and, more importantly, instant.
Surviving a kernel restart is what distinguishes this from , and kernel restarts are frequent — they are the correct response to confusing state.
Add to , and remember that the cache now contains every prompt and response, which is a data-retention question if the prompts contain anything sensitive.
The escape hatch matters for the case where you want to see the variation between samples at a high temperature.
The cache is keyed on the payload, so identical requests return identical answers — correct for reproducibility, wrong when variance is the thing you are studying.
The fuller version of this is in caching model responses in Python.
Getting the code out into a module The single change that most improves a notebook is moving the functions into a file next to it and importing them. makes that painless: edit the module in your editor, re-run the cell, and the new definition is live without restarting the kernel.
What moves out, in order of how much it helps: Prompts.
First, always.
A prompt in a module is diffable, greppable, importable by the service and by a test, and reviewable in a pull request.
A prompt in a cell is none of those.
Parsing and validation.
These are the functions with edge cases, which means they are the functions that want tests, which means they cannot live in a notebook.
The client and the call wrapper.
So the notebook and production send byte-identical requests.
If they diverge, the notebook’s findings do not transfer.
Nothing else, yet.
Plotting, slicing and eyeballing are what the notebook is for.
Moving those out too early is how people conclude notebooks are not worth using. re-executes module code but cannot re-bind everything: objects created before an edit keep their old class, and changes to class hierarchies or to definitions often need a kernel restart.
When behaviour stops matching the source you are reading, restart before debugging — it is the explanation surprisingly often.
Hygiene that costs nothing Practice Description nbstripout --