Migrating from nbdev to juplit
This guide describes how to migrate a project that uses nbdev (Jupyter-based literate programming with #|export directives) to juplit's percent-format workflow.
Overview
nbdev notebooks use special directives inside cells to control what gets exported:
#|export— cell is exported to the Python module#|hide— cell is hidden in docs (usually tests/setup)- No directive — cell is shown in docs but not exported (usually examples/tests)
In juplit, all cells are regular Python and everything in the file is importable. Test/example code is gated with if test(): instead of being in non-exported cells.
Migration steps
1. Initialize a new juplit project
pip install cookiecutter juplit
cookiecutter gh:DeanLight/juplit_template
cd <new_project_slug>
uv sync
poe init
2. For each nbdev notebook, create a paired .py file
For each .ipynb in the nbdev nbs/ directory, create a corresponding .py file in the new module directory using the conversion rules below.
Markdown cells → # %% [markdown] cell:
# %% [markdown]
# # Module Title
#
# Cell content here.
Code cells with #|export → regular # %% cell (strip the directive):
# nbdev: → # juplit:
# #|export # %%
# def my_func(x): def my_func(x):
# return x return x
Code cells without #|export (examples, tests, #|hide cells) → # %% cell wrapped in if test():
# nbdev (no #|export): → # juplit:
# assert my_func(1) == 1 # %%
# if test():
# assert my_func(1) == 1
3. Add the jupytext header
Every converted file needs the header at the top, followed by the test import:
# ---
# jupyter:
# jupytext:
# formats: ipynb,py:percent
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.16.0
# kernelspec:
# display_name: Python 3
# language: python
# name: python3
# ---
# %%
from juplit import test
# (other imports your module needs)
4. Generate notebooks and verify
poe sync # generates .ipynb from .py files
poe test # run tests to verify nothing broke
5. Update imports in the rest of the project
nbdev exports from a generated module path. After migration, the module is the .py files directly. Update any from nbs.xx_module import ... to from your_module import ....
Example conversion
Before (nbdev nbs/00_core.ipynb):
# Cell 1 — markdown
# # Core module
# Cell 2 — #|export
# #|export
# def add(a, b):
# return a + b
# Cell 3 — no directive (test shown in docs)
# assert add(1, 2) == 3
# Cell 4 — #|hide (hidden test)
# #|hide
# assert add(-1, 1) == 0
After (juplit your_module/core.py):
# ---
# jupyter:
# jupytext:
# formats: ipynb,py:percent
# ...
# ---
# %% [markdown]
# # Core module
# %%
from juplit import test
# %%
def add(a, b):
return a + b
# %%
if test():
assert add(1, 2) == 3
# %%
if test():
assert add(-1, 1) == 0
Install the migration skill for Claude Code
If you want Claude Code to assist with the migration automatically:
mkdir -p .claude/skills
juplit skill-migrate > .claude/skills/juplit-migrate.md