I wanted to launch a bunch of Python notebooks without having to carefully manage virtualenvs and kernels, so I made a [single file, hermetic notebook](https://gist.github.com/zsol/d624b49a505e1041f6948df37d74398e). You use it by downloading the file and issuing
```
uv run nb.py
```
(assuming you have [uv](https://docs.astral.sh/uv/) installed)
It will pop up a jupyter notebook in your browser, and away we go!
![[Hermetic Notebook Screenshot.png]]
## Things you can do
Apart from the usual Jupyter things, you can:
### Add more dependencies
```
uv add --script nb.py numpy scipy matplotlib
```
Or edit the `dependencies` block in the first cell, and rerun `uv run nb.py`.
### Check it into a repo
This notebook is a plain Python file, so unless you use top-level `await` expressions, it will work just fine (`%` magics are automatically commented out).
### Make it executable
On Linux/Mac, you could add a shebang like this:
```
#!/usr/bin/env -S uv run
```
to the top of the file, and after making it executable with `chmod +x nb.py`, you can directly run it: `./nb.py`
### Lint, Format, Type check
These tools are pre-installed in the notebook for you - formatting works using `black` and `isort`, type checking via `mypy`, and linting via `ruff`.
## How does it work
At the top of the file, there are a few things:
* [Inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata) captures dependencies, which `uv run` [reads and installs](https://docs.astral.sh/uv/guides/scripts/#running-a-script-with-dependencies) them into an ephemeral virtualenv (including the Python version if it's missing)
* [Jupytext](https://jupytext.readthedocs.io/en/latest/) headers allow the `.py` file to be read as a notebook
* the small bit of Python code at the top initializes Jupyter, isolates its configuration from `$HOME/.jupyter` and makes it open `.py` files using Jupytext by default, and opens itself when launching Jupyter
* `# %% jupyter={"source_hidden": true}` makes Jupyter hide (collapse) this first cell by default
Everything after the last `# %%` marker is the second cell of the notebook.
Apart from this, the dependencies include `black`, `isort`, and `jupyterlab-code-formatter` to power autoformatting of cells, `python-lsp-server` to provide IDE services using Jedi, and `pylsp-mypy` to make type checking happen.