A notebook has to answer one question before it can answer anything else: which Python? If you have ever watched a cell fail on an import and then found that package sitting right there in your virtual environment, you know why it matters.
Verso's answer, since 1.1.3, is the one you already have. Python cells run against a CPython installation on your machine, 3.8 or newer, in a separate process that Verso starts and supervises. We do not embed a Python of our own and we do not bundle one, so a cell sees the interpreter, virtual environment, and packages you would get from a terminal.
That sounds like a packaging detail, and it is the reason for everything else here. Running Python outside the notebook process is what lets you interrupt a cell, survive a native crash, and get a restart that genuinely clears imported module state.
A separate process, on purpose
Verso starts the host and talks to it over a loopback connection. Completions and hover are computed in that same process, so the interpreter that runs your code is the one that answers questions about it.
A restart clears the variables from earlier Python cells, but not values other languages shared into the notebook's variable store, because the store lives on the notebook's side of that boundary. It also takes with it anything the interpreter started, so a background shell command ends there rather than outliving it.
Which interpreter you get
Verso goes looking the first time a Python cell runs, and takes the first candidate that validates.
| Order | Source | Where it comes from |
|---|---|---|
| 1 | Explicit setting | verso.python.interpreterPath in VS Code, or PythonExecutable when embedding the engine |
| 2 | Environment override | The VERSO_PYTHON environment variable |
| 3 | Session selection | #!python <path> run earlier in this session |
| 4 | Active virtual environment | VIRTUAL_ENV |
| 5 | Active conda environment | CONDA_PREFIX |
| 6 | Workspace environment | A .venv or venv directory beside the notebook |
| 7 | Search path | python3 or python on PATH |
| 8 | Well-known locations | Standard install directories for the platform |
A candidate has to answer a version probe and report CPython 3.8 or newer, so a broken entry near the top of the list does not stop a working one further down from being found, and if nothing validates the cell tells you what it searched. Activation is read from the environment, so activating a virtual environment before you launch your editor is usually the whole configuration step.
#!python reports the interpreter in use, its version, what kind of environment it is, and which source selected it. #!python --list shows every interpreter that was discovered and where each was found. #!python /usr/local/bin/python3.13 selects one and restarts the kernel.
A selection like that lasts for the session and is never written into the notebook file, because an absolute path is only true on the machine that produced it. To pin an interpreter for a run instead, verso run, verso serve, and verso repl all take --python <path>.
Packages land in that same interpreter
Because the cell runs in your interpreter, an install lands there too, and the import on the next line finds it without a restart.
#!pip requests pandas>=2
import requests
#!pip runs before the rest of the cell and takes the same package specifiers and options pip does. If uv is on your PATH, Verso uses it. Both installers list every dependency they touch, so the cell reports a summary instead and saves that with the notebook. A failed install always prints in full, since that output is the only account of why it failed.
The second route is the import itself. A cell that imports something the environment does not have raises a "Package Install Required" dialog naming the distribution, the import that asked for it, and the interpreter it would go into. The distribution name is often not the import name, so import cv2 offers opencv-python, import PIL offers Pillow, and import sklearn offers scikit-learn. Decline and the cell runs anyway, failing at the import as it would have without the offer. An import wrapped in try is never installed: that cell has already said it can run without the module.
verso.python.autoInstall |
What happens | Where it applies |
|---|---|---|
prompt |
Asks first, listing the exact distributions and the environment. The default. | The editor |
auto |
Installs recognized distributions without asking, and reports a name it only guessed at rather than installing it. | verso run --auto-install |
off |
Never scans a cell's imports and never installs. | verso run |
Guessing means falling back to the import name as the distribution name, and that is the case to be careful about: a typo like import pandsa produces a plausible package name somebody may well have published. It is also why the command line differs from the editor, which matters if you run the same notebook in CI that you run at your desk.
The third route is declaring requirements once, either in the Python kernel's Dependencies setting or inline at the top of the first cell you run, using the standard script metadata format. Those travel with the file, so a declared requirement may only name a package. An installer option such as --index-url, or a location to install from such as a URL or a filesystem path, is reported rather than installed. Where code gets fetched from is a decision for whoever runs the notebook, not for the file they opened.
Some interpreters, particularly ones from a Linux distribution's package manager, mark themselves externally managed, and pip refuses to install into them. Verso detects that and derives a small environment from the interpreter with access to its existing packages. It never does that to a virtual environment, which is yours to install into.
Widgets draw
Libraries that render only as ipywidgets models hand the notebook a reference rather than a picture, which is why a cell ending in plot.display() used to print a model description. Those now draw: k3d, ipyleaflet, pythreejs, bqplot, and ipyvolume among them, as does anything built with anywidget.
import k3d
import numpy as np
plot = k3d.plot()
plot += k3d.points(np.random.randn(5000, 3).astype(np.float32), point_size=0.03)
plot.display()
The widget gets an auto-resized frame of its own, with theme tokens injected into it so it follows the host theme. Its state is saved with the notebook, so reopening the file draws it again without running the cell.
Two things to watch for. The state travels in the notebook, but the JavaScript that draws it comes from a public CDN, so a machine with no network draws an empty frame. And state has a size: a five thousand point scatter adds roughly 90 KB each time the cell is run and saved.
Next to a Jupyter kernel
Jupyter's own documentation describes a model worth putting beside this one: kernels there are "programming language specific processes that run independently", and an open notebook "has exactly one interactive session connected to a kernel". The nbformat specification records that choice in the file, as a single kernelspec in notebook metadata.
The difference is where the choices live. Verso's Python process is one of eight kernels reading and writing one variable store, so a value set in C# is readable in Python with no hand-off. And the interpreter is picked per session and per machine, never saved into the file.
Two notes if you are upgrading
Python cells now need CPython 3.8 or newer installed on the machine. Where none is found, a Python cell reports what it searched instead of failing silently.
PythonKernelOptions.PythonDll is ignored. The property remains, so nothing fails to compile, and a session that sets it says once that it has no effect.
Point a cell at the environment you already have and see whether it picks the interpreter you expected. If it does not, #!python --list will show you why. The rest is in the Python Interpreters and Python Packages guides.