Tracking Jupyter notebooks with Git
Keeping .ipynb files clean, diffable, and reviewable
A short tour of the options: strip outputs, strip metadata, or pair with a text file.
Why notebooks fight with version control
An .ipynb file is JSON that stores code, results, and metadata all together — and Git sees all of it. That causes four recurring problems.
Bloated repositories
Embedded plots and images are base64 blobs. Every rerun rewrites them, so the repository grows fast — and Git keeps every version forever.
Painful merge conflicts
Two people run the same notebook, and execution counts, outputs, and metadata all collide. Nothing about the code conflicted, but Git can’t tell.
Hard code review
Reviewers wade through thousands of lines of output JSON to find the few lines of code that actually changed.
Huge, noisy diffs
A one-line code edit can show up as a 500-line diff once outputs and counters are regenerated.
Option 1: Strip outputs with nbstripout
What it does
- Removes all cell outputs (and execution counts) so only code and structure are committed.
- Runs automatically before each commit, or as a Git filter on the working tree.
- Simplest, lowest-friction fix; kills the bloat and most merge noise.
Trade-off: rendered results no longer live in the repository, so you lose the at-a-glance view on GitHub.
Setup
In .pre-commit-config.yaml:
repos:
- repo: https://github.com/kynan/nbstripout
rev: 0.8.1
hooks:
- id: nbstripout
files: ".ipynb"pre-commit installOr install it as a Git filter directly, with no pre-commit involved:
nbstripout --installOption 2: Keep outputs, strip the noisy metadata
When you do want results in the repository
Tutorials and shared analyses are more useful when the rendered figures show up on GitHub. In that case, strip only the volatile bits.
What’s noisy: execution counts, kernelspec, language_version, widget state, signature, and editor metadata — all of which change constantly even when the code doesn’t.
How to do it
Use nbstripout flags to keep outputs but drop counts and metadata, or set a Git filter via .gitattributes so it is transparent and repository-wide:
# .gitattributes
*.ipynb filter=nbstripout
*.ipynb diff=ipynb
nbstripout --install --attributes .gitattributes
nbstripout --keep-output --keep-count # selective strippingSee the --keep-output and --keep-metadata-keys flags in the nbstripout docs.
Option 3: Pair with a text file using Jupytext
What it does
- Represents a notebook as a plain
.py(or.md) script, paired and kept in sync with the.ipynb. - Commit only the
.pyfile: clean line-by-line diffs, easy review, trivial merges. - The
.pyreads as normal code with# %%cell markers, soblack,ruff, andisortall work on it.
Trade-off: outputs aren’t stored at all; collaborators regenerate them by running the notebook.
Setup
# one-off pairing
jupytext --set-formats ipynb,py:percent nb.ipynb
# keep them in sync
jupytext --sync nb.ipynbAs a pre-commit hook:
- repo: https://github.com/mwouts/jupytext
rev: v1.17.3
hooks:
- id: jupytext
args: [--sync]Which one should you reach for?
| Approach | Outputs in repo? | Diff quality | Best when… |
|---|---|---|---|
| nbstripout (strip outputs) | No | Good | Code-focused projects; you just want the bloat gone |
| Strip metadata only | Yes | Better | Tutorials and shared analyses where rendered results matter |
| Jupytext pairing | No (commit .py) |
Best | Real code review, heavy collaboration, linting and CI |
They combine. A common setup is Jupytext to pair and sync, with nbstripout also in pre-commit. Pick per repository, not per career.
Go home and try this
Pick one repository with notebooks and set up a pre-commit hook this week.
- Add pre-commit. You already saw it for linting in this school; notebooks plug into the same
.pre-commit-config.yaml. - Strip first. Start with
nbstripout— instant relief from bloat and most merge conflicts, at near-zero effort. - Pair when reviewing. Add Jupytext when notebooks need real code review or live in CI.
Further reading
- nbstripout
- Jupytext
- pre-commit framework
- Jupytext pre-commit guide
- nbdime — notebook-aware diff and merge