Environment Management¶
Each notebook has its own isolated Python environment managed by uv.
How It Works¶
When you create a notebook, Strata:
- Generates a
pyproject.tomlseeded with the packages a cell needs to hand values to the next one:pyarrow,orjsonandcloudpickle - Runs
uv syncto create a.venv/anduv.lock - All cell execution uses this notebook-local venv
Opening a notebook runs the same uv sync (and restores renv.lock, when the notebook has one) as an environment job, which keeps the server responsive to other requests and tabs while it runs; the open answers once the job finishes, so cells can run straight away. The job appears in the Environment panel's history. Reopening a notebook that is already open skips the job unless its .venv is missing or its renv.lock changed.
Opening someone else's notebook runs its build code
In personal mode that uv sync builds any dependency that has no wheel, and the notebook itself when its pyproject.toml declares a [build-system]. A build runs the package's build backend as you, with your environment, before any cell runs. Before you open or import a notebook from someone you don't trust, read its pyproject.toml (or, for an .ipynb, the %pip install lines and imports that become its dependencies). Service mode installs wheels only (UV_NO_BUILD=1; see Service mode).
A hash of the lockfiles (uv.lock, plus renv.lock when the notebook has one) participates in provenance, so changing the environment invalidates all cached cell outputs. Only the lock's Python version (requires-python) and the runtime dependency closure of uv.lock are hashed (each package's version, source and artifact hashes), so moving the notebook to another Python version invalidates it, and adding a dev tool such as pytest, the first one included, leaves the cache alone. The notebook's own project entry (its name from pyproject.toml) is left out, so two notebooks with the same resolved dependencies get the same hash, and a team cache serves one's results to the other.
A cell sent to a remote worker runs in this same locked environment when the worker supports it, so the lock describes what the cell ran against there too.
Python Version¶
At notebook creation time, you can select a Python version from the versions configured on the server. The first one listed is the default.
To change it later, click Requested Python in the Environment panel. Strata rewrites requires-python, rebuilds .venv/ with uv sync, and restores the old version if the sync fails.
Note
The available versions come from the server's STRATA_NOTEBOOK_PYTHON_VERSIONS. Unset, they are the Python versions uv reports as installed on the server that Strata supports, with the server's own version included. A UV_PYTHON set in the server's environment does not apply to a notebook's uv commands; requires-python decides its interpreter.
Installing Packages¶
From the UI¶
Open the Environment panel in the sidebar. Type a package name and click Add.
The operation runs asynchronously, you can continue editing cells while it installs.
On a server in service mode, packages install from wheels only: a package with no wheel for the notebook's Python fails to resolve, and the message says a wheel is required. R packages are not added from the notebook there, nor on a personal-mode server with STRATA_NOTEBOOK_HARNESS_USER set: the server restores the notebook's committed renv.lock as the harness user when the notebook opens. See Service mode.
Import from requirements.txt¶
In the Environment panel, click Import and paste a requirements.txt:
Strata previews the changes (additions, removals, unchanged) before applying. The
import replaces the notebook's direct dependencies, except that pyarrow, orjson
and cloudpickle (which every Python cell needs) stay unless the file pins them itself.
Import from environment.yaml¶
Conda-style environment.yaml files are supported on a best-effort basis. Strata translates each conda spec to a pip requirement (numpy=2.1 becomes numpy==2.1, a conda-forge:: prefix is dropped) and adds the entries of the pip: list as they are. It ignores channels, the python pin and the pip entry itself, and the preview lists what it ignored.
Export¶
Click Export to download the current dependencies as requirements.txt.
Environment Operations¶
All environment mutations run as async jobs with seven actions:
| Action | Description |
|---|---|
add |
Install a new package |
remove |
Remove a package |
sync |
Rebuild the environment from pyproject.toml |
import |
Bulk import from requirements.txt or environment.yaml |
change_python |
Switch the notebook's Python version |
r_init |
Set up renv for R cells |
r_add |
Install an R package with renv |
The UI shows:
- Current job status (running, success, failed)
- Recent operation history (persisted across server restarts)
- Resolved package count and lockfile hash
Cache Invalidation¶
When you install or remove a package:
uv syncruns to updateuv.lock- The lockfile hash changes
- All cells become stale (their provenance no longer matches)
- Re-running any cell recomputes with the new environment
Switching back
If you remove a package and then re-add it (returning to the same uv.lock), the original provenance hashes match again and cells get cache hits. This is free by construction, no special logic needed.
Missing Package Detection¶
When a cell fails with ModuleNotFoundError, Strata detects the missing package and offers a one-click install button:
File Layout¶
my_notebook/
├── pyproject.toml # Package declarations
├── uv.lock # Locked dependency graph
└── .venv/ # Virtual environment (auto-created, not committed)
The pyproject.toml and uv.lock are the source of truth. The .venv/ is recreated by uv sync when needed.
Shared environments¶
On a server with many notebooks built from the same few lockfiles, a .venv
per notebook installs the same packages again and again. With
STRATA_NOTEBOOK_ENV_BACKEND=shared, notebooks share one environment per
lockfile instead. They live under STRATA_NOTEBOOK_SHARED_ENV_DIR, by default
envs beside the notebook storage directory (~/.strata/envs unless
STRATA_NOTEBOOK_STORAGE_DIR is set):
~/.strata/envs/
├── 3f9c…/ # one environment per uv.lock + interpreter build
├── 3f9c….lock # held while that environment is installed
└── refs/3f9c…/ # one file per notebook linked to it
my_notebook/
└── .venv -> ~/.strata/envs/3f9c…
- The key is a SHA-256 of
uv.locktogether with the exact interpreter build and platform. It leaves out the notebook project's own name, version and declared version ranges, the only part of the lock that differs between two notebooks with the same dependencies. Everything that changes what is installed counts: every resolved package with its version, source and hashes, the markers and extras on the notebook's dependencies, andrequires-python. So two notebooks with the same resolved dependencies on the same interpreter get the same environment, whatever they are called, and the second one's sync is only the link. - The notebook's own project is not installed into a shared environment
(
uv sync --no-install-project), since other notebooks link to it. A notebook created bystrata newhas no build system, so uv never installs it anyway. If you add one, its package is not importable from a shared environment. - A shared environment is never changed in place. Adding or removing a
package updates that notebook's
pyproject.tomlanduv.lockwithout syncing, then syncs, which lands in another environment and moves only that notebook's link. The other notebooks keep theirs. - One install at a time per environment. A notebook that needs an environment another notebook is still installing (an import opened straight away on a new server, say) waits for that install in its own environment job, then only links. The server keeps answering other requests meanwhile.
- Clean-up. An environment no notebook links to is removed once it has
gone unused for
STRATA_NOTEBOOK_SHARED_ENV_TTL_DAYS(default 7), by an hourly sweep in the server or bystrata env gc(--dirnames the store,--ttl-daysoverrides the TTL). An environment a notebook links to is never removed, opening a notebook counts as using it, and the sweep removes only environments it built. A directory of your own under the store, or one whose build never finished, is left alone.
R libraries are shared the same way. A notebook with an renv.lock restores
it once per server into r/ in the same store, keyed by the raw renv.lock
bytes and the exact R build, and its renv/library is a link there:
~/.strata/envs/
├── r/
│ ├── 8a41…/ # one library per renv.lock + R build
│ ├── cache/ # renv's package cache (RENV_PATHS_CACHE)
│ └── refs/8a41…/
my_notebook/
└── renv/library -> ~/.strata/envs/r/8a41…
A second notebook with the same renv.lock links to the library without
running renv::restore(), and every Rscript (cells, the warm pool, the
environment panel) reads it through renv's project path. Installing a package
from the environment panel first moves the notebook onto a private library
restored from the package cache, and once renv.lock is written that library
is kept under the new lock's key, so the other notebooks keep theirs. The R and
Python keys are separate: changing one lock rebuilds only that language's
environment. The same sweep removes libraries no notebook links to.
Provenance still follows the lockfiles, as with a .venv and renv/library
per notebook. POSIX only, since the link is a symlink.