Notebook in Neovim
I use LazyVim as my Neovim. This is a reproducible setup for editing and running Jupyter notebooks in it.
uvmanages Python and environments.jupytext.nvimopens.ipynbas readable text.molten-nvimruns the kernel and shows output.NotebookNavigator.nvimmoves between and runs# %%cells.- A dedicated Python environment hosts Neovim's Python remote plugins.
Architecture
Apple/system Python
-> leave alone
uv-managed Python
├── Neovim python-host
│ ├── pynvim
│ └── jupyter-client
│
└── project .venv
├── ipykernel
├── torch
└── project dependencies
uv tool
└── jupytext CLI
LazyVim
├── jupytext.nvim .ipynb -> # %% buffer
├── NotebookNavigator.nvim
└── molten-nvim
↓
Jupyter kernel
↓
project .venvKeep the three environments separate:
project/.venv: PyTorch and application dependencies.python-host: Neovim and Molten integration.jupytext: a user-wide CLI tool.
Leave Apple's Python alone
macOS ships its own Python:
python3 --version # Python 3.9.x
which python3 # /Library/Developer/CommandLineTools/usr/bin/python3Do not replace or modify it. Warnings that mention ~/Library/Python/3.9/bin come from packages installed into Apple's Python user environment; do not add that directory to PATH. Use uv for your own Python.
Install uv, Python and Jupytext
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version
uv python install 3.12
uv python listuv downloads Python automatically when a command needs it; installing it explicitly makes the setup visible. Docs: installation, Python versions.
jupytext.nvim calls the external jupytext executable, so install it once as a uv tool, independent of any project environment:
uv tool install jupytext
uv tool update-shell # then restart the shell
jupytext --versionProject environment
cd ~/path/to/project
uv python pin 3.12
uv venv --python 3.12
uv pip install ipykernel torch torchvision numpy matplotlibuv finds .venv in the current or a parent directory, so activation is optional. Activate it (source .venv/bin/activate) only to call python or jupyter directly; which python should then print /path/to/project/.venv/bin/python. Docs: environments.
The project needs neither pynvim (it belongs in Neovim's host) nor a local Jupytext. For a project with pyproject.toml, use uv add and uv sync so dependencies are reproducible.
Register the project as a Jupyter kernel:
uv run python -m ipykernel install --user --name dl-inference --display-name "DL Inference"
uv run jupyter kernelspec listThe list shows the default python3 kernel and dl-inference. The kernelspec stores an interpreter path:
cat ~/Library/Jupyter/kernels/dl-inference/kernel.jsonIts argv should point to the project's .venv/bin/python. Re-run the install command if the project moves.
Neovim's Python host
Molten requires pynvim and jupyter_client:
uv venv ~/.local/share/nvim/python-host --python 3.12
uv pip install --python ~/.local/share/nvim/python-host/bin/python pynvim jupyter-client
~/.local/share/nvim/python-host/bin/python -c "import pynvim, jupyter_client; print('OK')"Point Neovim to it in ~/.config/nvim/lua/config/options.lua:
vim.g.python3_host_prog = vim.fn.expand("~/.local/share/nvim/python-host/bin/python")Restart Neovim, then check:
:echo g:python3_host_prog
:checkhealth providerPlugin config
~/.config/nvim/lua/plugins/jupyter.lua:
return {
{
"goerz/jupytext.nvim",
version = "0.2.0",
opts = {
format = "py:percent",
},
},
{
"benlubas/molten-nvim",
version = "^1.0.0",
lazy = false,
build = ":UpdateRemotePlugins",
init = function()
vim.g.molten_output_win_max_height = 15
vim.g.molten_auto_open_output = true
vim.g.molten_virt_text_output = true
end,
},
{
"GCBallesteros/NotebookNavigator.nvim",
dependencies = {
"benlubas/molten-nvim",
},
event = "VeryLazy",
config = function()
local nn = require("notebook-navigator")
nn.setup({
activate_hydra_keys = nil,
repl_provider = "molten",
syntax_highlight = true,
})
vim.keymap.set("n", "]h", function()
nn.move_cell("d")
end, { desc = "Next notebook cell" })
vim.keymap.set("n", "[h", function()
nn.move_cell("u")
end, { desc = "Previous notebook cell" })
vim.keymap.set("n", "<leader>jr", function()
nn.run_cell()
end, { desc = "Run notebook cell" })
vim.keymap.set("n", "<leader>ja", function()
nn.run_and_move()
end, { desc = "Run cell and move next" })
end,
},
}- jupytext.nvim supports
format = "py:percent". - NotebookNavigator supports Molten directly; setting
repl_provider = "molten"prevents it from picking another provider later. lazy = falsekeeps Molten's remote-plugin registration predictable.
Install and register the plugins, then restart Neovim:
:Lazy sync
:UpdateRemotePlugins
:qaMolten is a remote plugin. Upstream notes that :UpdateRemotePlugins and a restart are needed after the first install and after relevant updates.
Health checks:
:LazyHealth
:checkhealth jupytext
:checkhealth provider
:checkhealthWorking in a notebook
nvim notebook.ipynbThe buffer shows Python with cell markers instead of JSON:
# %%
import torch
x = torch.ones(3)
x
# %% [markdown]
# # Tensor Basics
#
# **Markdown** cells are comments.# %% starts a code cell; # %% [markdown] starts a Markdown cell. Jupytext recommends this percent format for code-heavy notebooks.
Start the kernel inside Neovim, not in the shell:
:MoltenInit dl-inference| Keys | Action |
|---|---|
Space j r |
Run current cell |
Space j a |
Run current cell and move down |
]h / [h |
Next / previous cell |
:w |
Save back to .ipynb |
Molten commands: :MoltenInfo, :MoltenEvaluateLine, :MoltenEvaluateVisual, :MoltenReevaluateCell, :MoltenEnterOutput.
Gitsigns caveat
jupytext.nvim is not compatible with gitsigns.nvim on converted .ipynb buffers. If Gitsigns misbehaves there, stop it attaching to .ipynb files:
return {
{
"lewis6991/gitsigns.nvim",
opts = function(_, opts)
local old_on_attach = opts.on_attach
opts.on_attach = function(bufnr)
if vim.api.nvim_buf_get_name(bufnr):match("%.ipynb$") then
return false
end
if old_on_attach then
return old_on_attach(bufnr)
end
end
end,
},
}Troubleshooting
zsh: command not found: :MoltenInit
A Neovim command was run in the terminal. Open the notebook with nvim, then run :MoltenInit dl-inference inside Neovim.
E492: Not an editor command: MoltenInit
Molten is not registered. Check the host with the import pynvim, jupyter_client command from §5; reinstall pynvim or jupyter-client there if either is missing. Then, inside Neovim:
:checkhealth provider
:Lazy sync
:UpdateRemotePlugins
:qaRestart and test with :MoltenInfo.
.ipynb opens as JSON
Check that jupytext.nvim is installed, that the config sets format = "py:percent", and that jupytext --version works in the shell. :checkhealth jupytext reports both.
Missing ~/Library/Jupyter/runtime/kernel-....json
Jupyter could not create its runtime file. Create the directory and restart Neovim:
mkdir -p ~/Library/Jupyter/runtime
chmod 700 ~/Library/Jupyter/runtimeKernel starts but imports fail
The kernelspec points to the wrong environment, often after the project moved. Check kernel.json and re-run the ipykernel install command from inside the project.
State feels inconsistent
Jupyter kernels are stateful. Restart the kernel and run the cells again in order.
Fresh-machine checklist
# uv and Python
curl -LsSf https://astral.sh/uv/install.sh | sh
uv python install 3.12
# Jupytext CLI
uv tool install jupytext
uv tool update-shell
# Neovim Python host
uv venv ~/.local/share/nvim/python-host --python 3.12
uv pip install --python ~/.local/share/nvim/python-host/bin/python pynvim jupyter-client
# Jupyter runtime directory
mkdir -p ~/Library/Jupyter/runtime
chmod 700 ~/Library/Jupyter/runtimeAdd the host to options.lua and create jupyter.lua, then run :Lazy sync, :UpdateRemotePlugins and :qa in Neovim.
Per project:
cd ~/path/to/project
uv python pin 3.12
uv venv --python 3.12
uv pip install ipykernel torch torchvision numpy matplotlib
uv run python -m ipykernel install --user --name project-name --display-name "Project Name"
nvim notebook.ipynb # then :MoltenInit project-nameMaintenance
uv python upgrade 3.12 # Python patch releases
uv tool upgrade jupytextIn Neovim, :Lazy update; after a Molten update, :UpdateRemotePlugins and restart.