Skip to main content

juplit

Literate programming for Python — write in notebooks, commit clean Python, keep AI agents fast.

Why juplit

Jupyter notebooks are great for development: you can write prose next to code, run cells incrementally, and explore interactively. But .ipynb files are JSON blobs that create real problems:

  • Git history is cluttered — every output change, cell execution count, or metadata tweak shows up as a diff
  • AI agents struggle — JSON notebooks are token-heavy and hard to reason over compared to plain Python
  • Code review is painful — notebooks don't diff cleanly in pull requests

juplit gives you the best of both worlds. You write in jupytext percent-format .py files — plain Python that AI agents can read and reason over efficiently. You generate .ipynb files locally for interactive Jupyter sessions, but keep them out of git. The .py file is always the source of truth.

Installation

pip install juplit

CLI usage

juplit nb      # generate .ipynb from .py files (run after cloning)
juplit sync    # sync .py <-> .ipynb after editing
juplit clean   # sync then delete all .ipynb files (before AI agent sessions)
juplit skill   # print the Claude Code skill file for juplit

Project setup (pyproject.toml)

For a new project, use the cookiecutter template

[project]
dependencies = ["juplit>=0.1.0"]

[dependency-groups]
dev = ["poethepoet>=0.25.0", "pytest>=8.0.0", "ipykernel>=6.0.0", "pre-commit>=3.0.0"]

[tool.poe.tasks]
init  = {cmd = "pre-commit install"}
sync  = {cmd = "juplit sync"}
nb    = {cmd = "juplit nb"}
clean = {cmd = "juplit clean"}
test  = {cmd = "pytest"}

[tool.juplit]
notebook_src_dir = "your_module"   # directory juplit scans for paired .py files

[tool.jupytext]
formats = "ipynb,py:percent"

[tool.pytest.ini_options]
python_files = ["*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]

juplit finds the nearest pyproject.toml by walking up from the current directory, so the CLI works from any subdirectory.

Separating logic from tests with test()

Use test() to gate inline test code so it runs interactively in Jupyter and under pytest, but never on import:

from juplit import test

# %%
def add(a: int, b: int) -> int:
    return a + b

# %%
if test():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0
    print("add() tests pass")

pytest picks up these blocks automatically when you configure:

[tool.pytest.ini_options]
python_files = ["*.py"]

No def test_* functions required — just if test(): blocks next to the code they test.

You can also mix standard pytest functions with if test(): scaffolding blocks. Because if test(): runs at module scope during pytest collection, variables it sets up are available to def test_* functions:

from juplit import test

# %%
def compute(x: int) -> int:
    return x * 2 + 1

# %%
if test():
    inputs   = [1,  3,  -1]
    expected = [3,  7,  -1]

def test_compute():
    for x, e in zip(inputs, expected):
        assert compute(x) == e

Paired notebook format

A .py file is recognized as a paired notebook when its header contains:

# ---
# jupyter:
#   jupytext:
#     formats: ipynb,py:percent
# ...
# ---

Cells are delimited with # %% (code) and # %% [markdown] (prose).

Claude Code integration

Generate a skill file for Claude Code so it understands the juplit workflow:

juplit skill > .claude/skills/juplit-programming.md

For a skill on how to migrate nbdev repos to juplit:

juplit skill_migrate > .claude/skills/juplit-programming-nbdev-migrate.md

Metadata

Release files for juplit 0.0.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for juplit 0.0.8
File Size Uploaded
juplit-0.0.8.tar.gz 13.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for juplit 0.0.8
File Interpreter ABI Platform
juplit-0.0.8-py3-none-any.whl Python 3 none any Details

Total release size: 29.6 kB

Release files / juplit-0.0.8.tar.gz

Download URL juplit-0.0.8.tar.gz
Size 13.1 kB
Tags Source
SHA-256 checksum
How to use checksums
8406b20e5c10d518cdfc8b7a7dda6f522344b0288949406b84d8666b54193bf5
BLAKE2b-256 checksum
How to use checksums
174d3527157e7467df1638b554bebb7897664a1465ba51d8bda0e15e0de0962b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.17

Release files / juplit-0.0.8-py3-none-any.whl

Download URL juplit-0.0.8-py3-none-any.whl
Size 16.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ccb4ef05a81a6bb879c33cbc6ac2fa1d177d830b16fd64bb382b34c136d766a4
BLAKE2b-256 checksum
How to use checksums
290db5aa3279411e15717d719f40d8b2d0c0378d7e0cc8f3a89c18a69eaff1aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.17

Release history Release notifications | RSS feed

0.3.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

This release

0.0.8 This release

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page