This guide explains how to use jupytext to pair .ipynb notebooks with .py companion files for better git diffing and Claude Code editing.
- Git diffs of
.ipynbfiles are messy: Execution outputs, cell metadata, and JSON noise obscure actual code changes - Claude Code file size limits: Large notebooks with outputs can exceed token limits
- Merge conflicts: Notebooks with outputs create unnecessary conflicts
Jupytext creates a two-file system:
.ipynb- Full notebook with outputs (local development,.gitignore'd or committed).py- Clean Python representation (version controlled, diffed, edited)
✅ For Git: Diff/review .py files with clean code (no output noise)
✅ For Claude Code: Edit .py files directly (no token limit issues)
✅ For Humans: Keep .ipynb with outputs for local Jupyter development
✅ Auto-sync: Changes to either file automatically update the other
# Pair single notebook
~/bin/nb_pair.sh examples/basic/my_notebook.ipynb
# Pair all notebooks in directory
~/bin/nb_pair.sh examples/**/*.ipynbThis creates:
my_notebook.ipynb(original, with outputs)my_notebook.py(new, clean code with# %%cell markers)
Recommended: Commit BOTH files
# No .gitignore changes needed
# Commit both .ipynb (with outputs) and .py (clean code)
git add notebook.ipynb notebook.pyWhy commit both?
- ✅ Outputs are valuable (show results without re-running)
- ✅ Reviewers can see both code and results
- ✅
.pyfile provides clean diffs for code review - ✅
.ipynbprovides rendered outputs on GitHub - ✅ Best of both worlds!
Only ignore .ipynb if:
- Outputs are huge (>10MB) or contain sensitive data
- Notebooks change frequently with trivial output differences
- CI/CD regenerates outputs automatically
# Only if you have a specific reason:
# *.ipynbOption A: Edit in Jupyter (recommended)
# Edit notebook in Jupyter as usual
jupyter lab
# Jupytext auto-syncs .ipynb ↔ .py on save
# Commit BOTH files
git add examples/basic/my_notebook.ipynb examples/basic/my_notebook.py
git commit -m "Update analysis query"Option B: Edit .py directly (for Claude Code)
# Claude edits the .py file
# Then sync back to .ipynb:
~/bin/nb_pair.sh --sync examples/basic/my_notebook.py
# Or use jupytext directly:
jupytext --sync examples/basic/my_notebook.py# Pair notebook (creates .py companion)
~/bin/nb_pair.sh notebook.ipynb
# Sync after editing either file
~/bin/nb_pair.sh --sync notebook.ipynb
# Remove pairing (keeps .py file)
~/bin/nb_pair.sh --unpair notebook.ipynb# Pair with percent format (# %% cell markers)
jupytext --set-formats ipynb,py:percent notebook.ipynb
# Sync changes between files
jupytext --sync notebook.ipynb
# Convert without pairing
jupytext --to py:percent notebook.ipynbThe .py companion uses percent format:
# %% [markdown]
# # My Notebook Title
# This is a markdown cell
# %%
import pandas as pd
print("This is a code cell")
# %%
# Another code cell
result = pd.DataFrame({'a': [1, 2, 3]})Benefits of percent format:
- Valid Python file (can run with
python notebook.py) - Clear cell boundaries (
# %%) - Claude Code can edit directly
- Git diffs show actual code changes
Enable better notebook diffs in git:
# Configure jupytext as diff driver
git config diff.jupytext.command 'jupytext --to md --set-formats - -o -'
# Add to .gitattributes (already done)
echo '*.ipynb diff=jupytext' >> .gitattributes- Pair notebook:
~/bin/nb_pair.sh notebook.ipynb - Edit in Jupyter: Work normally, outputs saved to
.ipynb - Commit .py: Auto-synced on save, commit this file
- Review: Git diffs show clean code changes
Best for: Active development with outputs
- Pair notebook:
~/bin/nb_pair.sh notebook.ipynb - Claude edits .py: No token limits, clean diffs
- Sync back:
~/bin/nb_pair.sh --sync notebook.ipynb - Run in Jupyter: Execute to generate outputs
Best for: Large refactoring, architecture changes
# Pair all existing notebooks
find examples -name "*.ipynb" -exec ~/bin/nb_pair.sh {} \;
# Add to git
git add examples/**/*.py
git commit -m "Add jupytext pairing for all notebooks"
# Optionally ignore .ipynb files
echo "*.ipynb" >> .gitignoreA: Manually sync:
~/bin/nb_pair.sh --sync notebook.ipynb
# or
jupytext --sync notebook.ipynbA: Unpair and delete .py:
~/bin/nb_pair.sh --unpair notebook.ipynb
rm notebook.pyA: Resolve in .py file, then sync:
# Fix conflict in notebook.py
git add notebook.py
~/bin/nb_pair.sh --sync notebook.ipynbA: Edit the .py file directly:
- Claude reads
notebook.py(clean, no outputs) - Makes changes
- Sync:
~/bin/nb_pair.sh --sync notebook.py - Run in Jupyter to regenerate outputs
Both tools complement each other:
- nb_source_diff.py: Diff
.ipynbfiles without outputs (one-off) - jupytext pairing: Permanent
.pycompanions (ongoing)
Use jupytext for:
- Claude Code editing (avoids token limits)
- Normal git workflow (commit .py, diff .py)
- Long-term maintenance
Use nb_source_diff.py for:
- Quick diffs of unpaired notebooks
- Legacy notebooks you don't want to pair
- Ad-hoc comparisons
Auto-sync on commit:
# .git/hooks/pre-commit
#!/bin/bash
for ipynb in $(git diff --cached --name-only | grep '.ipynb$'); do
jupytext --sync "$ipynb"
git add "${ipynb%.ipynb}.py"
doneexamples/basic/
├── oc_parquet_analysis_enhanced.ipynb (full notebook with outputs)
└── oc_parquet_analysis_enhanced.py (clean code, version controlled)
Git workflow:
# Edit in Jupyter
jupyter lab examples/basic/oc_parquet_analysis_enhanced.ipynb
# Auto-synced to .py on save
# Commit clean code
git add examples/basic/oc_parquet_analysis_enhanced.py
git commit -m "Add geographic classification analysis"
# PR reviewers see clean Python diff, not JSON noise- ✅ DO: Pair notebooks you actively develop
- ✅ DO: Commit BOTH
.ipynband.pyfiles (usually!) - ✅ DO: Review
.pydiffs in PRs for code changes - ✅ DO: Check
.ipynbdiffs for output changes - ✅ DO: Let Claude Code edit
.pyfiles directly ⚠️ CONSIDER: Ignoring.ipynbfiles if outputs are huge/sensitive- ❌ DON'T: Manually edit both files separately (sync instead)
- ❌ DON'T: Commit
.ipynbAND.pywith conflicting changes
- Jupytext docs: https://jupytext.readthedocs.io/
- Helper script:
~/bin/nb_pair.sh - Diff tool:
~/bin/nb_source_diff.py - This guide:
/Users/raymondyee/C/src/iSamples/isamples-python/JUPYTEXT_WORKFLOW.md
Last Updated: 2025-10-15 by Claude Code