Inspiration

Most of what I know about ML I picked up from other people's notebooks. Kaggle kernels, the notebooks that come with books, random tutorials. The problem is they stop working after a couple of years. You open one, run the first few cells, and something has been removed from pandas or scikit-learn.

Getting it to run again usually isn't that bad. What always bugged me was that once it ran, I had no idea if it was still doing the same thing. I actually ran into this while building Revive. Deleting one argument that scikit-learn had dropped made the code run, but it quietly switched to a different algorithm. And a KMeans notebook gave different clusters without a single error.

So I wanted something that fixes the notebook and then checks it against the author's own saved outputs, cell by cell. And for notebooks shared without outputs, something that can go back in time and get them.

What it does

You give Revive a broken notebook. It:

  1. Runs it in its own Python environment and finds the first cell that fails.
  2. Looks up why. Nemotron Nano writes a search query and Tavily finds the changelog or migration note.
  3. Asks Nemotron Ultra for a fix. Ultra sees the code, the error, the installed versions, the search results and any deprecation warnings the notebook printed back when it was written, and sends back a small patch.
  4. Runs it again, and keeps going as long as each attempt gets further than the last.
  5. Checks every cell against the outputs the author saved, with a small tolerance for numbers. If a fix runs but prints different numbers, the exact difference goes back to the model. It only says "verified" when every cell matches.

The Time Machine. A lot of notebooks get shared with their outputs cleared, so there's nothing to check against. In that case Revive:

  1. Asks Nemotron which Python and library versions the notebook was probably written for, based on its code and when it was last changed.
  2. Installs exactly that old setup with uv, only allowing packages released before that date.
  3. Runs the original, untouched notebook there. Whatever it prints becomes the answer key for the fixed version.

That's how it catches the KMeans change in a notebook that has no outputs and runs fine today. A "does it run?" check would call that notebook working.

The web app shows the repair as it happens. Cells go red when they fail, get tagged when Nemotron edits them, and go green when they match (or amber, with the difference shown). The log has every search, diff and rebuild, and you can download the fixed notebook and a JSON report.

You can also upload your own notebook along with the data files it reads. Before running anything, Revive reads it and tells you what would get in the way: a missing CSV or image, a framework it doesn't support yet, or a file path on the author's computer. Uploads run in a sandbox that can't see the server's API keys.

The Time Machine verifying a notebook that was saved without outputs

Related Work. Revive takes ideas that other people have already explored a few steps further. nbval checks notebook outputs but doesn't fix anything. SnifferDog restores old notebook environments. MLEModernizer repairs Kaggle notebooks with an LLM but only checks the final score. The 2026 study Containing the Reproducibility Gap compares re-run outputs with saved ones but doesn't repair them. What I couldn't find was a repair loop that has to pass a cell-by-cell output check, or one that uses the original notebook running in its rebuilt old environment as the reference.

How I built it

  • NVIDIA Nemotron on Nebius Token Factory, through the OpenAI-compatible API. Nemotron 3 Ultra (550B, 55B active) does the hard parts: working out what broke, writing patches and guessing old environments. Nemotron 3 Nano (30B, 3B active) writes a search query on every attempt. Revive picks the model IDs from /v1/models when it starts, retries temporary server errors and tracks tokens per model.
  • Tavily finds the changelog, migration guide or GitHub issue for each error while the repair runs. The sources show up next to each fix.
  • uv builds the Time Machine's old environments (old Python, exact old library versions, --exclude-newer set to the notebook's date) in seconds.
  • nbclient and Jupyter kernels run notebooks cell by cell, each repair in its own environment.
  • The output check compares each cell's printed output with the reference. Numbers are compared within a tolerance. Things that change for no real reason are ignored: timings, memory addresses, NumPy 2 printing np.float64(...), plot placeholders. Formatting changes that libraries made on purpose are recognised too, like Int64Index becoming Index, or scikit-learn only printing the parameters you set.
  • Rules for every patch. Broken Python is rejected, and cells that already match are locked. Patches that paste the expected output into the code, override __repr__, monkey-patch or subclass library objects, swap plt.show() for something that only prints a figure, or bring back code that already failed are all rejected, and the model is told why.
  • A pre-flight check reads an uploaded notebook without running it: files it opens, frameworks it needs, missing modules, downloads. The repair button stays off until nothing blocks the run, so credits don't get spent on a run that can't work.
  • A sandbox for uploads. Uploaded notebooks run as a separate user that can't read the API keys. I tested it with a notebook that tries to, including through /proc.
  • FastAPI for the web app, with live updates, instant replays of recorded runs and a daily cap on live runs. It runs in Docker on Render.
  • 308 automated tests with real kernels, real environments and real old-Python rebuilds, run on GitHub Actions on every push.

Challenges I ran into

  • Running isn't the same as correct. Early fixes ran fine and printed different numbers. Getting the comparison to ignore harmless noise but still catch real differences took a lot of back and forth.
  • The model tried to game the check. On real notebooks, Nemotron rewrote an error message so it would print pandas' old Int64Index, overrode __repr__ to print old scikit-learn output, and stuck the removed DataFrame.append back onto an object. All of those were aimed at the test, not the code. Revive now rejects them and tells the model why.
  • It went in circles. On one notebook it deleted a removed argument (the numbers changed), put it back (crash), then deleted it again. Now Revive remembers every version of a cell that failed and won't let the model bring one back.
  • It "fixed" things that weren't broken. Once it decided np.random.RandomState had been removed (it hasn't) and replaced it, which changed the data. Cells that already match are locked now.
  • Some cells are supposed to fail. A tutorial cell that shows a TypeError on purpose has to keep raising it, so Revive now checks for the same error instead of "repairing" it.
  • Long notebooks break in lots of places. A fixed number of attempts ran out while the model was still making progress, so now it keeps going as long as each attempt gets further, and stops after four in a row that don't.
  • My own benchmark was a bit wrong. The first expected outputs came from modern rewrites of the old code. Running the original code on real old libraries showed one of them didn't match what old scikit-learn actually printed, so now the benchmark only uses real outputs.
  • Real notebooks need more than the notebook. When I looked for a notebook to demo uploads, most of them read a CSV, show an image from the author's folder, or print the date and library versions. An upload like that would just fail after a few minutes. That's why I built the pre-flight check and data file uploads, and why cells that only print versions aren't compared.
  • Old plots looked like missing output. Sebastian Raschka's 2017 clustering notebook saved its plots as <matplotlib.figure.Figure at 0x...>. Revive didn't recognise that as a plot, so it thought 11 cells had lost their output, and Nemotron started swapping plt.show() for code that just prints a figure. I fixed the comparison and added a rule against that trick, and the next run was verified.
  • Running strangers' code safely. The sandbox worked on my machine but broke on Render, which doesn't let root send signals to other users' processes. A small supervisor process fixed it.

Accomplishments that I'm proud of

  • Verifying a fixed notebook that was shared without any outputs, by rebuilding the environment it was written in.
  • Catching changes that don't throw errors (KMeans, one-vs-rest) which any "does it run?" check would miss.
  • Numbers I can stand behind: real expected outputs, failures reported as failures, and safeguards that came from reading what the model actually did.

Results

Benchmark: 20 notebooks, each broken on NumPy 2.5, pandas 3.1 and scikit-learn 1.9 (removed aliases, removed methods, renamed parameters, silent behaviour changes). Three were saved without outputs. The expected outputs are real ones, from running the old code on the old libraries.

Verified repairs 20/20
Saved without outputs, verified with the Time Machine 3/3
Rebuilt original matched the author's real (hidden) outputs 3/3
Fixes that ran but printed different numbers, caught and then corrected 3
Average attempts per notebook 1.35
Tokens for the whole benchmark 137,238

Real notebooks by other people. A benchmark I wrote myself only shows so much, so I also ran Revive on public notebooks written years ago, checked against the authors' own saved outputs.

Notebook Result
Python Data Science Handbook 3.04, missing values Verified, 26/26 cells. fillna(method=...) became .ffill() / .bfill(), and a cell that shows a TypeError on purpose still raises it.
Python Data Science Handbook 3.06, concat and append Verified, 14/14 cells. df1.append(df2) became pd.concat([df1, df2]), even inside a string the notebook evaluates.
Python Machine Learning (Raschka) chapter 11, clustering Verified, uploaded through the web app with its two images. AgglomerativeClustering(affinity=...) became metric=....
handson-ml chapter 6, decision trees Already working, 17/17 cells. Nothing needed changing; only scikit-learn's printout had changed.
handson-ml chapter 4, linear models Not repaired. It breaks in several places, and Nemotron's later patches pointed at code that wasn't in the cells.

What I learned

Getting an LLM to make old code run turned out to be the easy part. Making sure the fix didn't quietly change the results is where most of the work went, and that needs a check that doesn't trust the model. I also saw how agents actually go wrong: Nemotron sometimes tried to pass the test instead of fixing the code, and almost every rule in Revive came from reading what it did on real notebooks. And I learned to be honest about results, using real outputs, other people's notebooks, and reporting failures as failures.

What's next for Revive

  • Checking plots, not only printed output.
  • Notebooks that use PyTorch, TensorFlow or a GPU.
  • Fixing whole repositories (scripts, requirements.txt, tests), and a GitHub Action that runs Revive whenever dependencies are upgraded.

Built With

Share this project:

Updates

Submission history