Chapter 11 of 60

Hidden Notebook State

Concepts

CHAPTER 11 β€” HIDDEN NOTEBOOK STATE

PART III β€” Debugging Interactive and Numerical AI

PURPOSE

Hunts what Ch10’s gutter cannot show on the threshold == 0.9 ghost (warm kernel answers, zero visible definitions, clean Run All NameError): deleted/overwritten-cell residue, stale imports, and aliased mutation via the fresh-vs-dirty kernel diff.

CENTRAL QUESTION

When the kernel knows something no visible cell teaches it, what ordered probe names the ghost’s source?

UNIQUE CLAIM

Visible cells are the recipe and the kernel namespace is the kitchen β€” every execution adds ingredients, nothing but del/restart/overwrite removes them β€” so mystery bindings are convicted by diffing the dirty namespace dump (value/type/id) against visible text plus git log -p/%history, then a twice-repeated fresh-kernel prefix run whose three-way outcome (absent = H1 orphan history; present-but-different = H2 stale live definition; same = faulty search) names the provenance line; and repeat offenses mean the workload wants a reactive/linted notebook, because the imperative shared-namespace REPL is the architectural root cause.

DEBUGGING OBJECT

State as namespace-vs-text divergence β€” dirty dir()/%who residents (threshold/df/region_map) cross-checked against current cells, history, commit diffs, and sys.modules mtimes.

CONCEPTS INTRODUCED

Fresh-vs-dirty kernel diff probe; orphan (H1 deleted/overwritten history) vs stale (H2 live-but-aged definition/cached import) residue classes; provenance-line conviction (exec number/commit/import time, not stories); imperative-REPL root-cause framing; dataflow notebooks (explicit dependency encoding) and reactive notebooks (editβ†’re-run dependents, deleteβ†’scrub, state==code) as class fixes; “discipline fixes the incident, tooling fixes the class.”

CONCEPTS DEVELOPED / REUSED

Execution-order viewer from Ch10 (nbsafety lineage reused for orphan/stale detection); Run-All gating from Ch10 (fix verified by fresh Run All, never warm del); MEASUREMENT-vs-explanation from Ch3 (kernel answer is state measurement, not provenance); error-bar honesty (divergent fresh repeats = UNKNOWN).

PREREQUISITES

Ch10 (gutter, Run All), Ch3 (measurement discipline).

LOCAL INVARIANTS

Dump value/type/id before theorizing; search current text, then history, then commits; pre-write H1/H2 fresh-kernel predictions; two fresh-prefix runs (identical visible prefix); fix in visible text (restore/remove/reload policy), gate with fresh Run All; provenance, never plausibility.

FAILURE MODES

Text-search exoneration (“not in text, must be kernel bug” β€” absence IS the finding); warm-kernel patching (del ghost_var re-hiding the recipe gap); reload superstition (re-import without importlib.reload as no-op fix); in-place mutation blindness (functional-looking cleaner over mutated frames); single fresh-run conviction; agreement-as-provenance (“looks right, so fine”).

DIAGNOSTIC METHOD

  1. Dump dirty namespace. 2. Search text→history→commits. 3. Fresh-kernel prefix run ×2 with predictions. 4. Name the provenance line (deleted-cell diff / stale exec number). 5. Restore/remove/reload in visible text; fresh Run-All gate.

RESEARCH-DERIVED IDEAS

Koop & Patel TaPP 2017 dataflow notebooks (persistent cell IDs + rewritten refs β†’ explicit tracked reproducible graph); Macke et al. VLDB 2021 nbsafety (stale+orphan detection inside stock Jupyter β€” the automated diff); Chattopadhyay et al. CHI 2020 (hidden-state management as named top pain β€” design problem, not carelessness); Pluto.jl/Observable/marimo reactive notebooks 2018–2025 (delete scrubs, edit re-runs dependents; Pluto guarantee state==visible code; marimo enforces it by static AST analysis at the cost of forbidding cross-cell mutation + duplicate globals β€” the deleted-cell-residue class is designed out at a real tradeoff cost, not detected).

EXPERIMENT / LAB

Lab 11 (PROPOSED): deliberately planted ghost (ghost_var=42 executed, cell deleted or overwritten without re-run), H1 history-only vs H2 stale-live with distinct fresh predictions, dirty dump + two fresh-prefix runs + %history/git provenance. H-structure: independent var = kernel history (dirty vs fresh); controls = file revision, executed prefix, environment. Success = named provenance line + visible-text fix + green fresh Run All; warm-del is not completion.

COMPANION TOOL

Hidden State Inspector β€” accepts: dirty dump + cell texts + execution history + .ipynb git history. Can-establish: which names lack live definitions (orphans) vs drifted vs fresh (stale) + changed-since-import modules, under this revision/prefix only. Cannot-establish: data correctness, order health (Ch10), cross-machine reproducibility (Ch12), intent.

PREVENTION ARTIFACT

Inspector record + restored definition (or removed dependence / reload policy) + fresh-Run-All gate; repeat-offense trigger to adopt reactive/linted kernel.

READER OUTCOME

Reader can name the originating execution/commit of any mystery binding and fix it in visible text β€” testable via Lab 11’s namespace-diff record.

DEPENDENCIES

Ch10, Ch3.

FORWARD BRIDGE

Ch12 “Reproducible Notebooks” β€” inherits the sharing gap: clean order + explained state still fail on a colleague’s machine (unpinned kernel/seeds/data/time).

EVIDENCE / RESEARCH REQUIREMENTS

threshold ghost constructed illustration; reactive notebooks carry exploration/global-name tradeoffs; nbsafety eval 666-session bounded; UNKNOWN where history unrecoverable.

ANTI-CLAIMS / LIMITS

One diff convicts one residue class under one revision/prefix; certifies no ordering (Ch10), parity (Ch9), or future reproducibility; plausible-looking residue still residue.

Explain this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Apply this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

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.9 in the warm kernel; zero definitions in visible cell text; clean Run All raises NameError at the same cell. HYPOTHESIS H1 (deleted-cell residue): threshold was 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): threshold was 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:

  1. Deleted cells. Deleting a cell deletes text, not bindings. threshold = 0.9 ran 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.
  2. Overwritten definitions. Cell 4 once read threshold = 0.9; it now reads threshold = 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.
  3. Stale imports and aliased mutation. from utils import threshold ran before utils.py was edited to remove it (module object cached in sys.modules); or df was mutated in place by a cell whose visible text now shows a copy (df_clean = df.copy() added later, never re-run). Re-importing without importlib.reload re-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:

  1. Inventory the kitchen. In the dirty kernel, dump the namespace: %who, dir(), and for suspects threshold β€” type(), repr(), and id(). Record as MEASUREMENT.
  2. Inventory the recipe. Search all visible cell text (and git log -p on 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.
  3. 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.
  4. Convict with provenance, not assertion. The winning evidence is a git diff showing the deleted/edited cell, or a %history line 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 -p shows cell 8 threshold = 0.9 deleted in the cleanup commit; fresh kernel raises NameError at 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 by del threshold patching 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.

  1. Write H1 (history-only residue: deleted/overwritten cell) and H2 (stale live definition or cached import) with distinct fresh-kernel predictions before restarting.
  2. 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.
  3. 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 -p to name the originating execution.
  4. 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

  1. Dump before theorizing β€” value, type, and id() in the dirty kernel.
  2. Search text, then history β€” current cells first, git log -p and %history second.
  3. Fresh-kernel prefix run as the discriminating experiment; predictions pre-written.
  4. Name the provenance line β€” execution number or commit that planted the residue.
  5. 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_var then moving on. The recipe still omits the ingredient; the next warm session re-haunts differently.
  • Reload superstition. Re-running import utils and assuming fresh code. Without importlib.reload (or kernel restart), the cached module object persists β€” re-import is a no-op disguised as a fix.
  • In-place mutation blindness. Assuming df is 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 NameError blamed 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

Debugging Checklist

  • Dirty-namespace dump (value/type/id()) recorded before theorizing?
  • Current-text search + history/git log search 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.9 ghost β€” 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.