Hidden Notebook State
Part III β Debugging Interactive and Numerical AI
The variable that exists nowhere
Chapter 10 closed with a green, in-order Run All β gutter monotonic, cells top-to-bottom. The engineer reopens the same notebook a week later, runs the first six cells interactively, and types threshold in cell 7:
# cell 7 (fresh session, only cells 1-6 executed)
print(threshold)
# 0.9 <- where did this come from?
A search for threshold across every visible cell returns nothing. No definition, no import, no magic. Yet the kernel answers 0.9. Restart & Run All from the top fails at cell 7 with NameError: name 'threshold' is not defined β while the warm kernel insists the variable is real.
OBSERVATION:
threshold == 0.9in the warm kernel; zero definitions in visible cell text; clean Run All raisesNameErrorat the same cell. HYPOTHESIS H1 (deleted-cell residue):thresholdwas defined by a cell that was later deleted or overwritten β the kernel remembers what the document forgot. HYPOTHESIS H2 (out-of-order / stale-import residue):thresholdwas defined by a live cell executed under older text, or by a re-imported module whose current source no longer exports it β visible but stale. INFERENCE: none yet β%history,execution_count, and cell text must be cross-checked before either hypothesis earns belief. The kernel’s answer is a MEASUREMENT of state, not an explanation of provenance.
This chapter’s question: when the kernel knows something no visible cell teaches it, what ordered probe names the ghost’s source?
Why the obvious explanation fails
The obvious move β “search the notebook for the definition” β fails because the definition is not in the notebook. It is in the notebook’s past. Three mechanisms plant state no text search can find:
- Deleted cells. Deleting a cell deletes text, not bindings.
threshold = 0.9ran as[8], the cell was deleted during cleanup, and the binding survived. Chapter 10’s gutter cannot flag what is no longer displayed β the execution sequence has a hole shaped exactly like the missing cell. - Overwritten definitions. Cell 4 once read
threshold = 0.9; it now readsthreshold = load_config()["cutoff"]but was never re-executed after the edit. The kernel holds the old value under the new text. Outputs look current; bindings are fossils with fresh labels. - Stale imports and aliased mutation.
from utils import thresholdran beforeutils.pywas edited to remove it (module object cached insys.modules); ordfwas mutated in place by a cell whose visible text now shows a copy (df_clean = df.copy()added later, never re-run). Re-importing withoutimportlib.reloadre-binds nothing β the old module object persists.
| Mechanism | What was removed | What survived | Fresh-kernel tell |
|---|---|---|---|
| Deleted cell | the cell’s text | its bindings in the namespace | NameError β the name is absent entirely |
| Overwritten, not re-run | the old cell text | the old value, now under new text | name present, but value/type differs from the current text’s version |
| Stale import / in-place mutation | the module’s old source (or a pristine frame) | the cached module object / the mutated object | name present; value reflects pre-edit source or a prior mutation |
OPINION: a long-lived notebook kernel is a crime scene where the perpetrator tidied up. The mess is gone; the fingerprints are in
dir().
The mental model: visible cells are the recipe; kernel namespace is the kitchen. Every execution adds ingredients; nothing except an explicit del, a restart, or an overwrite removes them. Debugging hidden state means diffing the kitchen against the recipe and treating every unexplained resident as a suspect.
The method: the fresh-vs-dirty kernel diff
Ordered probe β residue source before content blame:
- Inventory the kitchen. In the dirty kernel, dump the namespace:
%who,dir(), and for suspectsthresholdβtype(),repr(), andid(). Record as MEASUREMENT. - Inventory the recipe. Search all visible cell text (and
git log -pon the.ipynb) for the suspect name. A definition in history but not in current text supports H1; a definition in current text with mismatched value supports H2. - Run the discriminating intervention: fresh-kernel execution to the same cell. Restart, run cells 1β7 top-to-bottom exactly once, and compare. Prediction if H1 (deleted residue): fresh kernel raises
NameErrorβ the ghost was history-only. Prediction if H2 (stale redefinition/import): fresh kernel defines the name but with a different value or type (the current text’s version), proving the dirty value was stale. Same value in both kernels would exonerate hidden state entirely β the definition was visible all along and the search was faulty. - Convict with provenance, not assertion. The winning evidence is a
git diffshowing the deleted/edited cell, or a%historyline showing the orphaned execution β a pointer to the originating run, not a story about it.
flowchart TD
D["dump the dirty kernel: value, type, id() per suspect"] --> T{"name in visible cell text?"}
T -->|"no"| H1c["H1 candidate: history-only residue"]
T -->|"yes, value/type mismatches the text"| H2c["H2 candidate: overwritten or stale import"]
H1c --> FR["restart; run the visible prefix 1..N exactly once"]
H2c --> FR
FR --> R{"suspect in the fresh kernel?"}
R -->|"absent (NameError)"| H1["H1 confirmed: restore the definition in visible text"]
R -->|"present, differs"| H2["H2 confirmed: fix re-execution / reload policy"]
R -->|"present, identical"| EX["hidden state exonerated: the text search was faulty"]
H1 --> P["name the provenance line: %history exec number or git commit"]
H2 --> P
# hidden-state diff probe (run in dirty kernel, save output, then restart and re-run to same cell)
import json
suspects = ["threshold", "df", "region_map"] # names under investigation
dirty = {name: repr(eval(name))[:200] for name in suspects if name in dir()}
print("DIRTY:", json.dumps(dirty, indent=1))
# After restart + run-to-same-cell, compare against FRESH dict built identically.
# Prediction H1: key present in DIRTY, absent in FRESH (NameError).
# Prediction H2: key present in both, values/types differ.
OBSERVATION (constructed illustration, not a measured run): dirty kernel reports
threshold == 0.9 (float);git log -pshows cell 8threshold = 0.9deleted in the cleanup commit; fresh kernel raisesNameErrorat cell 7. UPDATED BELIEF: H1 supported β deleted-cell residue. H2 suspended (no live definition exists to go stale). INFERENCE: the fix restores the definition as a visible cell (or deletes the dependence), verified by fresh-kernel Run All β never bydel thresholdpatching on the warm kernel, which hides the recipe gap.
Research lineage: the ghost is a design flaw, not a user error
Every mechanism in this chapter β deleted-cell residue, overwritten-but-not-rerun definitions, stale imports, in-place mutation β is a consequence of one design choice: the notebook uses an imperative REPL where each execution mutates a shared namespace and nothing links the namespace back to the visible code. Two research directions attack that root cause rather than the symptoms.
Make dependencies explicit. Koop and Patel’s dataflow notebooks give each cell a persistent identifier and rewrite in-cell references so the dependency graph between cells is encoded, tracked, and reproducible (Koop & Patel, 2017).
Make execution reactive. Observable (JavaScript), Pluto.jl (Julia), and marimo (Python) run the notebook as a dataflow graph: editing a cell re-runs its dependents automatically, and deleting a cell scrubs its variables from memory. Pluto states the resulting guarantee directly β at any instant, program state is completely described by the code you can see. marimo enforces the same by static analysis, at the cost of forbidding cross-cell mutation and duplicate global names. In those environments this chapter’s bug cannot occur; there is no history for the kernel to remember that the document has forgotten. The tradeoff is real β reactive notebooks give up the freewheeling out-of-order exploration Jupyter allows β but the class of ghost this chapter hunts is designed out rather than detected.
Detect staleness inside Jupyter. Where switching notebook systems is not an option, nbsafety (Macke et al., Chapter 10) brings the same lineage tracking to the standard kernel, flagging exactly the stale and orphaned bindings the fresh-vs-dirty diff hunts by hand.
The practical implication: if you run the fresh-vs-dirty diff more than occasionally on the same project, the finding is not “be more careful” β it is “this workload wants a reactive notebook or a linted kernel.” Discipline is the fix for the incident; tooling is the fix for the class.
Lab 11: fresh-kernel vs. dirty-kernel diff
PROPOSED, not executed: no author-measured results are reported. The evidence this chapter requires is the reader’s own namespace diff.
Setup. Build the ghost deliberately: in a scratch notebook define ghost_var = 42 in a cell, execute it, then delete the cell (or overwrite its text to ghost_var = int(open("config.txt").read()) without re-running). Confirm ghost_var still answers in the warm kernel. You now hold a known-origin ghost β the lab is the detection procedure, not the haunting.
Task.
- Write H1 (history-only residue: deleted/overwritten cell) and H2 (stale live definition or cached import) with distinct fresh-kernel predictions before restarting.
- Dump the dirty namespace for the suspect name(s): value, type,
id(). Independent variable: kernel history (dirty vs. fresh); controlled variables: file revision, execution prefix (cells 1βN identical), environment. - Restart and execute exactly the visible prefix to the same cell, once. Record OBSERVATION (value present/absent/different + verbatim error if any) and UPDATED BELIEF. Consult
%history/git log -pto name the originating execution. - Repeat the fresh run a second time to exclude one-off ordering slips; divergent repeats are UNKNOWN, not convictions.
| Kernel | Prefix executed | OBSERVATION (ghost_var) |
UPDATED BELIEF |
|---|---|---|---|
| dirty | ad-hoc history | ___ (value/type) | baseline, inadmissible |
| fresh #1 | cells 1βN verbatim | ___ / NameError: ___ |
H1 if absent; H2 if present-but-different |
| fresh #2 | cells 1βN verbatim | ___ | confirms; mismatch β UNKNOWN |
Success criterion. A named provenance line (deleted-cell diff or stale-execution number) plus a visible-text fix verified by a green fresh-kernel Run All. A warm-kernel del plus a passing single cell is explicitly not completion β it re-hides the gap.
Companion tool: Hidden State Inspector
What it accepts: the dirty-kernel namespace dump, the current cell texts, execution history (%history / execution_count log), and the git history of the .ipynb.
What it performs: it diffs namespace residents against visible definitions, flags names with no live definition (orphans, H1 suspects), names with value/type drift vs. fresh execution (stale, H2 suspects), and cached modules whose source changed since import; it refuses a clean bill while any resident is unexplained.
What it can establish: which names are unexplained by visible text and which class of residue (orphan vs. stale) each belongs to β under the examined revision and prefix only.
What it cannot establish: data correctness, execution-order health (Chapter 10’s viewer), cross-machine reproducibility (Chapter 12), or intent β an explained namespace can still compute the wrong thing. It never treats namespace agreement or import success as diagnosis.
How its output changes your next action: orphan rows route to restoring-or-removing the definition in visible text; stale rows route to re-execution discipline or importlib.reload + import policy; a fully explained namespace with a still-failing run routes to Chapters 12β13 with hidden state exonerated in writing.
Paper form, sufficient for this chapter:
Suspect name: ___ Dirty value/type: ___ / ___
Visible definition? Y/N (cell ___) History definition? Y/N (exec ___ / commit ___)
Fresh-kernel result: present-absent-different (circle) value/type: ___ / ___
Cached-module suspect: ___ (source mtime vs import time: ___)
CONVICTION: H1 orphan / H2 stale (circle; provenance line: ___)
Where a software implementation does not yet exist in the reader’s stack, this record is the tool. The namespace-vs-text discipline precedes any automation.
Reusable procedure: every mystery binding gets this
- Dump before theorizing β value, type, and
id()in the dirty kernel. - Search text, then history β current cells first,
git log -pand%historysecond. - Fresh-kernel prefix run as the discriminating experiment; predictions pre-written.
- Name the provenance line β execution number or commit that planted the residue.
- Fix in visible text (restore, remove dependence, or reload policy) and gate with fresh Run All.
Failure modes
- Text-search exoneration. “No definition found, so it must be a kernel bug.” The definition is in history, not in text β absence in current text is the finding.
- Warm-kernel patching.
del ghost_varthen moving on. The recipe still omits the ingredient; the next warm session re-haunts differently. - Reload superstition. Re-running
import utilsand assuming fresh code. Withoutimportlib.reload(or kernel restart), the cached module object persists β re-import is a no-op disguised as a fix. - In-place mutation blindness. Assuming
dfis pristine because the cleaning cell “looks functional.” Mutation history lives in execution order, not in current text β re-run the prefix or distrust the frame. - Single fresh-run conviction. One
NameErrorblamed on cosmic rays or one match treated as proof. Fresh runs are cheap; run twice, record both. - Agreement-as-provenance. “The value looks right, so the state is fine.” Correct-looking residue is still residue β provenance, not plausibility, is the standard.
Limits, per contract: one namespace diff convicts one residue class under one file revision and prefix; it does not certify ordering (Chapter 10), environment parity (Chapter 9), or future reproducibility. UNKNOWN where history is unrecoverable.
References
- David Koop and Jay Patel. Dataflow Notebooks: Encoding and Tracking Dependencies of Cells. 9th USENIX Workshop on the Theory and Practice of Provenance (TaPP), 2017. https://www.usenix.org/conference/tapp17/workshop-program/presentation/koop
- Stephen Macke, Hongpu Gong, Doris Jung-Lin Lee, Andrew Head, Doris Xin, and Aditya Parameswaran. Fine-Grained Lineage for Safer Notebook Interactions. Proceedings of the VLDB Endowment 14(6), 2021, pp. 1093β1101. https://doi.org/10.14778/3447689.3447712
- Souti Chattopadhyay, Ishita Prasad, Austin Z. Henley, Anita Sarma, and Titus Barik. What’s Wrong with Computational Notebooks? Pain Points, Needs, and Design Opportunities. Proceedings of the 2020 CHI Conference on Human Factors in Computing Systems, 2020. https://doi.org/10.1145/3313831.3376729
- Fons van der Plas et al. Pluto.jl: Simple Reactive Notebooks for Julia. 2020β. https://github.com/fonsp/Pluto.jl Β· Akshay Agrawal et al. marimo: A Reactive Python Notebook. 2023β. https://github.com/marimo-team/marimo
Debugging Checklist
- Dirty-namespace dump (value/type/
id()) recorded before theorizing? - Current-text search + history/
git logsearch both completed? - H1/H2 with distinct fresh-kernel predictions written before restart?
- Two fresh-prefix runs recorded with verbatim outcomes?
- Provenance line named (exec number / commit / import time)?
- Fix applied in visible text and verified by fresh Run All, not warm-kernel patch?
- No plausible-looking value accepted without provenance?
What This Chapter Established
- Hidden state as kitchen-vs-recipe divergence: deleted cells, overwritten-but-not-rerun definitions, cached imports, and in-place mutations plant bindings no text search finds.
- The fresh-vs-dirty kernel diff (dump β text+history search β predicted fresh-prefix runs Γ2 β provenance conviction), demonstrated on the
threshold == 0.9ghost β constructed illustration, no measured runs claimed. - Lab 11 as a proposed namespace-diff record the reader executes; the Hidden State Inspector contract (accepts/performs/can-establish/cannot-establish/next-action).
- What was NOT proved: execution-order health, cross-machine reproducibility, or computational correctness under an explained namespace.
- The class fix is architectural: dataflow (Koop & Patel) and reactive (Pluto, Observable, marimo) notebooks eliminate these ghosts by construction; nbsafety (Macke et al.) detects them inside Jupyter. Discipline fixes the incident; tooling fixes the class.
- Forward link: order is clean (Ch 10), state is explained (this chapter) β and the notebook still fails on a colleague’s machine. Explained state that only reproduces here is not reproducibility. That discipline is next.
Next
The kernel is clean, the namespace is explained, Run All passes β on this machine, today. Tomorrow it fails on a colleague’s laptop with different package versions, no pinned kernel, and an unseeded shuffle. Nothing in Chapters 10β11 prevents that, because neither chapter pins anything. The next chapter converts today’s green run into tomorrow’s guarantee: reproducible notebooks.