emjupy edits and runs Jupyter notebooks in Emacs. Plots and widgets
can be shown inline or in an external window or browser (see
Interactive output and GIFs and screenshots). emjupy uses Emacs’s own
tools – eglot, xref, eldoc, tramp – with whatever completion setup
you already have (vertico, corfu, orderless or anything else).
Completion, documentation and M-. come through eglot, from a language
server running beside the kernel.
It talks to a running Jupyter server over the same HTTP and WebSocket
API the browser uses, so a kernel on another machine works through an
ssh tunnel, and the notebook stays a standard .ipynb that any other
Jupyter tool opens.
…without touching your own Emacs configuration.
The repository ships a small notebook, test_notebooks/test.ipynb, with
a module beside it, so there is something real to run. To test with
your own notebooks, jump to step 4.
- Get emjupy.
git clone https://github.com/mathren/emjupy cd emjupy - A Python environment with Jupyter. Either create the one the
notebook comes with,
conda env create -f test_notebooks/environment.yml conda activate test_emjupyor install into one you have:
pip install jupyter-server ipykernel jupyter-lsp 'python-lsp-server[all]' matplotlib numpy tqdmjupyter-lspandpython-lsp-servergive completion, documentation andM-.; everything else works without them. - Start a server in the notebook’s directory, in its own terminal:
cd test_notebooks JUPYTER_TOKEN=abc jupyter server --no-browser --port=8888Or on another machine, the same way, in a copy of
test_notebooks/or any directory with notebooks, forwarding its port from this one:ssh -N -L 8888:localhost:8888 <remote machine>
- Start Emacs from the repository root:
emacs -Q -l src/quicktry.el-Qleaves your configuration out;quicktry.elputs emjupy on the load path and installswebsocket.elinto a temporary package directory, touching nothing in your setup. - Log in:
M-x emjupy-login RET 8888 RET, andabcwhen asked for the token. Picktest.ipynbfrom the list. - Try it:
C-c C-cruns the cell at point and moves to the next;M-.onexample_functionjumps to its definition inlibrary.py;C-c C-vshows every command, grouped by what it is for.
- persistent kernel session (to load large dataset once and re-use them)
- remote or local kernel (just needs port and token)
- in-line rendering of output including images
- adopts the kernel already running behind a port, and the Python environment it runs in
- completion, documentation and
M-.througheglot, from a language server running beside the kernel viajupyter-lsp - resilient to connection drops (remote kernel keeps running, reconnect Emacs)
- toggle hide/show output without re-running cells
- a debugger, as JupyterLab’s: breakpoints in cells, stepping, variables
- an outline of the headings, to jump through: imenu, and a panel beside the notebook
- interactive plotly figures open in a window of their own;
ipywidgetscontrols work in the buffer, and widgets that draw themselves open in a live page - every cell command – insert, delete, move, split, join – is undone and redone like any edit
ein-like workflow and key bindingsemjupy-notebook-list, likeein:notebooklist, to browse notebooks and the files beside them
Clone this repo, and add its src/ directory to load-path in your
init.el:
(add-to-list 'load-path "/path/to/emjupy/src")
(require 'emjupy)websocket.el must be installed too, from GNU ELPA or MELPA.
Or with use-package (without :rev :newest, the latest release rather
than the latest commit):
(use-package emjupy
:vc (:url "https://github.com/mathren/emjupy"
:lisp-dir "src"
:rev :newest)
:config
(setq emjupy-render-latex t)):lisp-dir "src" is needed: without it the package installs but
emjupy cannot be loaded.
Or with whichever package manager you already use – each installs
websocket.el too:
;; package-vc, built into Emacs:
(package-vc-install '(emjupy :url "https://github.com/mathren/emjupy"
:lisp-dir "src"))
;; elpaca:
(elpaca (emjupy :host github :repo "mathren/emjupy"
:files ("src/*.el" "src/*.py" (:exclude "src/quicktry.el"))))
;; straight.el:
(straight-use-package
'(emjupy :type git :host github :repo "mathren/emjupy"
:files ("src/*.el" "src/*.py" (:exclude "src/quicktry.el"))))The sources are in src/, so each is told where: :lisp-dir for
package-vc, :files for elpaca and straight – which also install the
figure window, emjupy-figure-window.py, and leave out quicktry.el, a
development tool.
Alternatively, M-x package-install-file on the tarball make package
builds (see Building & development).
emjupy speaks to a Jupyter server the way the browser does: HTTP for files and kernels, WebSockets for a kernel’s messages and the language server’s. Everything goes over the server’s one port, so a remote server needs one SSH tunnel and nothing started on the far side.
Emacs, on this machine Jupyter server, here or remote ┌─────────────────────────────────────┐ ┌───────────────────────────────────────┐ │ notebook buffer (emjupy-mode) │ HTTP │ Contents API ──── .ipynb files │ │ cells ⇄ structs ⇄ .ipynb JSON ────┼───────▶│ Sessions and Kernels APIs │ │ │ outputs, widget controls │ │ │ │ ▼ │ WS │ ZMQ │ │ kernel connection ────────────────┼───────▶│ /api/kernels/…/channels ──▶ kernel │ │ │ WS │ │ │ shadow buffer (.py) ◀── Eglot ──────┼───────▶│ /lsp/ws/… ── jupyter-lsp ──▶ pylsp │ │ │ │ │ │ figure window ◀── page ◀── bridge │ └───────────────────────────────────────┘ └─────────────────────────────────────┘ the bridge relays a widget page's messages over the kernel connection above
- The notebook buffer is the notebook: cells drawn as text, kept in
step with the cell structs, which are what is saved as
.ipynb. - The kernel connection carries execution and its output; widgets’ messages too.
- The shadow buffer is every code cell as one Python file, for Eglot,
whose language server runs beside the kernel through
jupyter-lsp. - The figure window shows what a text buffer cannot – plotly figures, widgets that draw themselves – and a widget’s page talks back through the bridge, a WebSocket server in Emacs, not to the Jupyter server.
| Feature | emjupy | ein | emacs-jupyter | ob-jupyter |
|---|---|---|---|---|
| Notebook-style buffer of cells | ✓ | ✓ | ✗ | ✗ |
Reads and writes standard .ipynb | ✓ | ✓ | ✗ | ✗ |
| Notebook list UI | ✓ | ✓ | ✗ | ✗ |
| Undo across cell operations | ✓ | ✗ | – | ✓ |
| Interactive widgets | ✓ | ✗ | ~ (in a browser) | ? |
| Kernel via Jupyter server HTTP + WebSocket (ssh tunnel is enough) | ✓ | ✓ | ✓ | ✓ |
No emacs-zmq dynamic module required | ✓ | ✓ | ✗ | ✗ |
eglot (completion, M-., rename across cells) | ✓ | ✗ | ✗ | ✗ |
| Kernels other than Python | ✗ | ✓ | ✓ | ✓ |
| Oldest Emacs supported | 30.1 | 26.1 | 27 | 27 |
| Maintained | ✓ | ✗ | ✓ | ✓ |
✓ = supported, ✗ = not supported, ~ = partly, – = does not apply (a REPL has no cell operations), ? = not documented. Checked <2026-10-03 Sat> against each project’s README and package header.
They differ in what they edit and how they reach the kernel. ein and
emjupy edit .ipynb notebooks as cells, over a Jupyter server’s HTTP
and WebSocket API: an ssh tunnel is enough. emacs-jupyter is a kernel
client for a REPL and Org-babel blocks, with no notebook buffer and no
.ipynb; it requires the emacs-zmq dynamic module, whose ZMQ cannot
reach a notebook server’s kernels, though it can use a server’s REST API
through TRAMP-style /jpy:host#port:name names; its widgets are
experimental, in a browser. For notebook-level eglot integration,
emjupy is Python only (other kernels untested), needs jupyter-lsp on
the server, and Emacs 30.1+.
ein was my working tool for over a decade, but it is
unmaintained: sunset since 2023, its architecture “fundamentally
incompatible with LSP”, its README says, pointing to xjupyter, a
successor at 0.0.1 running local kernels through a Python
jupyter_client helper, not a Jupyter server, and needing mode-overlay,
which GNU Emacs 30.1 lacks. I share code with non-Emacs users, so not
ob-jupyter, and emacs-jupyter does not suit me.
The full documentation lives in docs/:

