Notebook in Neovim

I use LazyVim as my Neovim. This is a reproducible setup for editing and running Jupyter notebooks in it.

  • uv manages Python and environments.
  • jupytext.nvim opens .ipynb as readable text.
  • molten-nvim runs the kernel and shows output.
  • NotebookNavigator.nvim moves between and runs # %% cells.
  • A dedicated Python environment hosts Neovim's Python remote plugins.

Architecture

In · text
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 .venv

Keep 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:

In · bash
python3 --version   # Python 3.9.x
which python3       # /Library/Developer/CommandLineTools/usr/bin/python3

Do 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

In · bash
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

uv python install 3.12
uv python list

uv 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:

In · bash
uv tool install jupytext
uv tool update-shell   # then restart the shell
jupytext --version

Project environment

In · bash
cd ~/path/to/project
uv python pin 3.12
uv venv --python 3.12
uv pip install ipykernel torch torchvision numpy matplotlib

uv 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:

In · bash
uv run python -m ipykernel install --user --name dl-inference --display-name "DL Inference"
uv run jupyter kernelspec list

The list shows the default python3 kernel and dl-inference. The kernelspec stores an interpreter path:

In · bash
cat ~/Library/Jupyter/kernels/dl-inference/kernel.json

Its 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:

In · bash
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:

In · lua
vim.g.python3_host_prog = vim.fn.expand("~/.local/share/nvim/python-host/bin/python")

Restart Neovim, then check:

In · vim
:echo g:python3_host_prog
:checkhealth provider

Plugin config

~/.config/nvim/lua/plugins/jupyter.lua:

In · 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 = false keeps Molten's remote-plugin registration predictable.

Install and register the plugins, then restart Neovim:

In · vim
:Lazy sync
:UpdateRemotePlugins
:qa

Molten is a remote plugin. Upstream notes that :UpdateRemotePlugins and a restart are needed after the first install and after relevant updates.

Health checks:

In · vim
:LazyHealth
:checkhealth jupytext
:checkhealth provider
:checkhealth

Working in a notebook

In · bash
nvim notebook.ipynb

The buffer shows Python with cell markers instead of JSON:

In · python
# %%
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:

In · vim
: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:

In · lua
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:

In · vim
:checkhealth provider
:Lazy sync
:UpdateRemotePlugins
:qa

Restart 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:

In · bash
mkdir -p ~/Library/Jupyter/runtime
chmod 700 ~/Library/Jupyter/runtime

Kernel 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

In · bash
# 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/runtime

Add the host to options.lua and create jupyter.lua, then run :Lazy sync, :UpdateRemotePlugins and :qa in Neovim.

Per project:

In · bash
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-name

Maintenance

In · bash
uv python upgrade 3.12   # Python patch releases
uv tool upgrade jupytext

In Neovim, :Lazy update; after a Molten update, :UpdateRemotePlugins and restart.