uv-stack
Reusable package sets for shared Python environments and uv projects — powered by micromamba and uv.
You define what you want in a few small files — profiles (named package
groups) and bundles (groups of profiles). Then you install those
definitions somewhere: a shared environment you activate (one stack.txt per
environment), or a uv project that carries its own .venv. The stack
command renders, locks, and installs everything else, splitting the work
between two tools:
| Tool | Owns |
|---|---|
micromamba |
The Python interpreter and conda-level binary packages (from conda-forge) |
uv |
Every pip-level package — compiled to a lock file, then synced exactly |
flowchart LR
subgraph sources["You define these"]
P["profiles/*.yaml"]
B["bundles/*.yaml"]
S["envs/NAME/stack.txt"]
T["pyproject.toml<br>tool.uv-stack"]
end
R{{"stack create env<br>stack upgrade"}}
EY["environment.yml"]
RI["requirements.in"]
LK["requirements.lock.txt"]
ENV[("shared env: NAME")]
R2{{"stack create project<br>stack refresh"}}
D["pyproject.toml<br>dependencies"]
V[("project .venv")]
P --> R
B --> R
S --> R
R --> EY
R --> RI
EY -- "micromamba create" --> ENV
RI -- "uv pip compile" --> LK
LK -- "uv pip sync + check" --> ENV
P --> R2
B --> R2
T --> R2
R2 -- "records what it applied" --> T
R2 -- "uv add --no-sync" --> D
D -- "uv sync" --> V
micromamba builds the room (the interpreter and any conda-level binaries);
uv furnishes it (fast, fully locked pip packages). The same profiles and
bundles feed both destinations. The box on the left is what you author —
inside [tool.uv-stack] that means the stack key, since the rest of that
table is uv-stack's own bookkeeping. requirements.in and environment.yml
are generated and carry a # Generated by uv-stack — do not edit. header,
and uv compiles requirements.lock.txt from requirements.in. On the
project path stack writes the resolved packages into your own
pyproject.toml and records what it applied back into [tool.uv-stack],
leaving the lock and .venv to uv.
Why you might want this:
- Define packages once, reuse everywhere. The same profile feeds your shared notebook environment and your uv projects.
- Shared environments are rebuildable. A shared environment is a pure
function of its source files. Delete it,
stack create env NAME, and it comes back. - Real locks. Every shared environment gets a fully pinned
requirements.lock.txt, compiled and installed withuv(fast), and verified withuv pip check.
- Installation
- Quickstart
- What you define
- Where it goes
- Everyday commands
- Configuration reference
- Tips and gotchas
Installation
Prerequisites (both must be installed and reachable):
uvmicromamba— runmicromamba shell initafter installing so$MAMBA_EXEis set
Then install the CLI as a standalone tool (requires Python 3.12+):
uv tool install uv-stack
(pip install uv-stack works too.) This gives you one command: stack.
Quickstart
Five minutes from nothing to a working, locked environment.
# One guided command sets up everything with prompts — a config tree, a
# commented 'starter' profile, and a 'main' environment (--yes accepts defaults):
stack init
# Prefer explicit steps? An equivalent setup by hand, with your own profile:
stack config init
stack create profile ds numpy pandas --description "Core data-science stack" --tag data
stack create env main ds --python 3.12
# Use it (stack prints this hint after every successful create env):
micromamba activate main
# ...or run things in it without activating:
micromamba run -n main python
What just happened: stack resolved the ds token to your profile, generated
requirements.in and environment.yml inside envs/main/, had micromamba
create the main environment (Python 3.12 by default), compiled a pinned
requirements.lock.txt with uv pip compile, installed it exactly with
uv pip sync, and verified consistency with uv pip check.
From now on, changing what's installed is always the same two steps: edit a source file (a profile, a bundle, or stack.txt), then run stack upgrade main. Check what needs rebuilding at any time with stack status.
What you define
Profiles
A profile is a named list of pip packages, stored as
profiles/<name>.yaml in the config root:
description: Core data-science stack # optional
tags: [data, core] # optional, used by `stack list --tag`
includes: # required: literal pip requirements
- numpy>=2
- pandas
Bundles
A bundle is a recipe that composes profiles, other bundles, and literal
packages, stored as bundles/<name>.yaml. Same schema, but includes entries
are stack tokens:
description: Everything for daily work
includes:
- ds # a profile
- chem # another profile
- utils
Bundles can nest other bundles; duplicates are removed automatically.
Stack tokens
Tokens are how you reference things in stack.txt, bundle includes, and on
the stack create project / stack resolve command line:
| Token | Meaning |
|---|---|
ds (bare name) |
Profile ds if it exists, else bundle ds, else a literal package |
profile:ds |
Profile ds (error if missing) |
@standard or bundle:standard |
Bundle standard (error if missing) |
pkg:numpy or package:numpy |
Always a literal package — the escape hatch when a name collides with a profile or bundle |
numpy>=2, -e ~/src/mytool, archive paths |
Literal pip requirement, passed through |
Use stack resolve to see how tokens are classified, and
stack resolve --full to expand them into a flat package list.
Where it goes
A definition is inert until you install it somewhere, and there are two
destinations. The choice is not "an environment or a project" — either way
you end up with an interpreter and a set of installed packages, and a project
carries its own .venv for exactly that. The real difference is where the
packages live: in one central, named place you activate from anywhere, or
inside a single directory alongside the code that uses them.
| Shared environment | Project | |
|---|---|---|
| Create with | stack create env NAME TOKENS... |
stack create project TOKENS... |
| Lives in | envs/<name>/ in your config root |
the current directory — pyproject.toml, uv.lock, .venv/ |
| Enter it with | micromamba activate <name> |
uv run ... |
| Update with | stack upgrade <name> |
stack refresh |
| Inspect with | stack show env <name>, stack status |
stack show project |
| Reach for it when | notebooks, ad-hoc work, tooling spanning many repos | one codebase, dependencies versioned and shipped with the code |
Shared environments are registered centrally under the config root, so
stack list env and stack status can enumerate them all. A project is
identified by the pyproject.toml in the directory you are standing in: there
is no registry to enumerate, and no parent-directory search — run
stack refresh and stack show project from the project root. That is also
why there is deliberately no stack list project.
Shared environments
A named shared environment lives in envs/<name>/ under the config root. It
exists as soon as stack.txt does.
Files you write:
| File | Purpose |
|---|---|
stack.txt |
Required. One stack token per line; # comments allowed |
python.txt |
Interpreter version (defaults to 3.12); applied at create and --recreate only |
micromamba.txt |
Extra conda packages (e.g. graphviz) |
channels.txt |
Extra conda channels (conda-forge is always first) |
requirements.local.in |
Machine-local pip additions, kept out of profiles |
Files stack generates (never edit these):
| File | Purpose |
|---|---|
environment.yml |
micromamba spec: python=<version>, pip, conda packages |
requirements.in |
Expanded, unpinned pip requirements |
requirements.lock.txt |
Fully pinned lock, produced by uv pip compile |
Projects
For per-project work, stack can seed a standard uv project from the same
profiles and bundles:
mkdir myproject && cd myproject
stack create project @standard pkg:httpx
This resolves the tokens to a flat package list, then runs uv init --bare,
uv add, and uv sync — leaving you a normal uv project with a
pyproject.toml, uv.lock, and .venv/.
Choosing the project's interpreter with --python:
- A version (
3.12), a path, or a spec likecpython@3.12is passed straight touv. - Anything else is treated as a micromamba environment name — the project
uses that environment's interpreter.
stack create project ds --python mainbuilds the project onmain's Python. - A botched version —
3.12.*,3.12.x— is not a version either, so it lands in the case above. It is still refused, but the message names the typo rather than telling you to create an environment called3.12.x.stack edit projectwarns about one in the tracking table when the editor exits. (Note that3.12.*is legal in an environment'spython.txt, which conda reads, not uv.) - With no
--python, the default comes from$UV_STACK_PROJECT_PYTHON, then<config-root>/project-python.txt, then3.12.
New projects are tracked by default: stack create project records
your tokens in a [tool.uv-stack] table inside pyproject.toml
(--no-track opts out for one-shot scaffolds). When your profiles or
bundles change later, re-resolve the project in place:
stack refresh # apply profile/bundle changes to this project
stack refresh --dry-run # see the add/remove delta first
Refresh removes only packages recorded in the table's applied list — the
ones uv-stack itself added. Dependencies whose names uv-stack never applied
are never touched. One caveat: ownership is by package name, so if you
re-pin a stack-applied package yourself (say uv add 'numpy<2' after a
profile applied numpy), uv-stack still owns that name and a later refresh
may rewrite or remove it. After an interrupted run, recovery adopts leftover pending names that are still installed but no longer in the stack — it warns first and the next successful stack refresh removes them (the warning tells you how to keep one). Day to day, the project is still a normal uv
project — uv add, uv sync, and uv run all work as usual.
Everyday commands
| Command | What it does |
|---|---|
stack init |
Guided first-run setup (config tree, starter profile, first env) |
stack create env NAME [TOKENS]... |
Scaffold (optional) and build a shared environment (--recreate wipes and rebuilds it: the lock is compiled first, and the conda layer is destroyed only if that succeeds) |
stack create profile NAME PKG... |
Write a new profile YAML (--description, --tag) |
stack create bundle NAME TOKEN... |
Write a new bundle YAML (--description, --tag) |
stack edit KIND [NAME] |
Open a profile, bundle, env source, or project file in your editor and validate it when the editor exits |
stack upgrade [NAMES]... |
Re-render, re-lock, and sync shared environments |
stack refresh |
Re-resolve a tracked project against current profiles/bundles |
stack status [NAMES]... |
Shared-env build state: drift, lock freshness, existence |
stack list env|profile|bundle |
Tables of what exists (--tag filters, --json for scripts) |
stack show env|profile|bundle [NAME] |
Details for one item (NAME defaults to main for envs) |
stack show project |
The tracked project in this directory: tokens, applied packages, pending state |
stack resolve [--full] TOKENS... |
Classify tokens, or expand them to a flat package list |
stack doctor [--fix] |
Detect problems; --fix applies the safe repairs |
stack completion bash|zsh|fish |
Print the shell-completion script |
stack config init |
Create missing config directories (bare primitive) |
Editing configuration
stack edit opens a config file in your editor and checks it the moment the
editor exits. If the file does not validate, the error is shown and the editor
is offered again, with your changes left in place. Nothing is reverted: if you
decline the re-offer, the file stays exactly as you left it and the command
exits non-zero. When stdin is not a terminal there is no re-offer at all — the
error is printed and the command exits 1. Quitting the editor with a non-zero
status (:cq in vi) aborts without validating at all.
stack edit profile ds # profiles/ds.yaml
stack edit bundle standard # bundles/standard.yaml
stack edit env # envs/main/stack.txt
stack edit env chem --file python # envs/chem/python.txt
stack edit project # ./pyproject.toml
On success the command names the file it validated and how to apply the change:
stack upgrade NAME for an environment, stack refresh for a tracked project,
and for a profile or bundle a reminder that it takes effect on the next upgrade
or refresh.
--file selects which environment source to open and accepts stack
(default), python, micromamba, channels, and local
(requirements.local.in). It applies to env only. An optional source that
does not exist yet is opened anyway, so this is how you add a
channels.txt. edit never creates a profile, bundle, or environment — use
stack create for that.
Symlinked config files work, so a profile can live in a dotfiles repo and be linked into the config root. When the file you name is itself a symlink, the success line reports the real file it resolves to rather than the link, so you can see where the write landed. A link pointing at nothing is refused instead of followed.
The editor is chosen from the first of these that is set:
--editor$UV_STACK_EDITOR<config-root>/editor.txt$VISUAL$EDITOR
The value is a command line, so code -w and emacsclient -nw both work; a
value that is spelled as a path — absolute, ~-prefixed, or containing a / —
and names an existing file is used as a single argument, so a path containing
spaces needs no quoting, and a leading ~ is expanded to your home directory.
The edited file is appended as the final argument. An empty
environment variable or editor.txt is skipped rather than treated as a
choice, so an exported-but-empty $VISUAL does not shadow $EDITOR. If none
of the five is set, stack edit says so and stops — it will not drop you into
an editor you did not choose.
editor.txt uses the same line grammar as the other config-root text files:
the first non-blank line wins, and everything from a # onward is a comment.
An editor command containing # would therefore be truncated, so set one of
the environment variables instead if you need one.
echo 'code -w' > ~/.config/python-envs/editor.txt
Use your editor's blocking flag. stack edit validates the file at the
moment the editor process exits. A GUI editor that hands the file to an
already-running instance and returns immediately — code without -w,
subl without -w, gvim without -f — exits before you have typed
anything, so validation runs against the file as it was and reports success
prematurely. This cannot be fixed from uv-stack's side; the fix is the flag:
stack edit profile ds --editor 'code -w'
Upgrading
stack upgrade main # one environment
stack upgrade main scratch # several
stack upgrade # every environment (lists them, asks first)
stack upgrade --all -y # every environment, no prompt
stack upgrade --no-upgrade main # re-lock without floating pins upward
stack upgrade --upgrade-package numpy main # upgrade only numpy
stack upgrade --dry-run main # print the command plan
By default upgrade recompiles the lock with --upgrade (everything floats
to the newest allowed versions), then syncs the environment to match the lock
exactly — packages you removed from a profile are uninstalled. A batch keeps
going past a failing environment and ends with a ✓/✗ summary
(--stop-on-error aborts at the first failure).
stack upgrade refuses a non-recreate upgrade when the running interpreter does
not match python.txt — use stack create env NAME --recreate to resolve
the mismatch first.
Changing an environment's Python version
To upgrade or downgrade an existing environment's interpreter:
stack create env NAME --python 3.14 --recreate
--recreate requires python.txt to hold a plain dotted version such as 3.14;
a conda match spec such as 3.12.* is refused.
The command compiles the candidate lock before destroying the environment, so an
unsatisfiable resolve leaves the old environment intact. micromamba.txt,
channels.txt, and python.txt are applied at environment creation and
--recreate only; stack upgrade re-syncs the pip layer without rebuilding
the conda layer.
Inspecting
$ stack resolve standard ds numpy
bundle:standard
profile:ds
package:numpy
$ stack resolve --full standard
numpy
pandas
rdkit
rich
$ stack list profile --tag data # only profiles tagged 'data'
$ stack show env main # python, tokens, channels, resolved packages
Checking your setup
stack doctor
doctor never changes anything — it reports problems (missing directories,
legacy file formats, environments in the wrong place) with a suggested fix:
line for each.
Watching for drift
stack status
Shows one row per shared environment: whether the micromamba env exists, whether a
lock is present, and whether your sources changed since the last build
(sources changed means "run stack upgrade"). When the running interpreter
does not match python.txt, the state becomes python changed and the Python
column shows both the configured and actual versions (e.g. 3.12 (env 3.14.0)).
--json makes every inspection command (list, show, resolve, status,
doctor) script-friendly.
Typo protection
A bare token that matches no profile or bundle becomes a literal PyPI
package. If it looks like a near-miss of one of your names, stack warns
(did you mean 'standard'?). Add --strict to upgrade, create env,
create project, create bundle, or resolve to turn any unqualified
fallthrough into an error, and use the pkg: prefix to say "yes, really a
package."
Shell completion
# zsh — add to ~/.zshrc:
eval "$(stack completion zsh)"
Tab-completes commands, options, and your environment/profile/bundle names.
Configuration reference
Config root
All state lives under one directory, resolved in this order:
--root PATH(global flag)$UV_STACK_ROOT$UV_ENV_ROOT(the historical spelling, still accepted)~/.config/python-envs(default)
~/.config/python-envs/
├── project-python.txt # optional: default --python for `create project`
├── editor.txt # optional: editor command for `stack edit`
├── profiles/
│ └── <name>.yaml
├── bundles/
│ └── <name>.yaml
├── .locks/ # internal lock files (safe to leave alone)
└── envs/
└── <name>/
├── stack.txt # required (defines the env)
├── python.txt # optional
├── micromamba.txt # optional
├── channels.txt # optional
├── requirements.local.in # optional
├── requirements.in # generated
├── environment.yml # generated
└── requirements.lock.txt # generated
Shell environment variables
| Variable | Purpose |
|---|---|
UV_STACK_ROOT |
Config root (overridden by --root) |
UV_ENV_ROOT |
The historical spelling of UV_STACK_ROOT; used only when UV_STACK_ROOT is unset or empty |
UV_STACK_PROJECT_PYTHON |
Default interpreter spec for stack create project |
UV_STACK_EDITOR |
Editor command for stack edit; beats editor.txt, $VISUAL, and $EDITOR |
VISUAL |
Standard fallback editor for stack edit, below editor.txt |
EDITOR |
Standard fallback editor for stack edit, consulted last |
MAMBA_EXE |
Path to the micromamba binary; set by micromamba shell init and preferred over PATH lookup |
Tips and gotchas
- Edit sources, not generated files.
requirements.in,environment.yml, andrequirements.lock.txtare overwritten on every upgrade. For env-specific additions userequirements.local.in; for anything reusable, a profile. --dry-runis almost dry. It runs no commands and never touches the lock or the environment, but it does re-renderrequirements.inandenvironment.yml.- Your lock is safe from failed compiles. The lock is compiled to a
temporary file and atomically swapped in, so a failed
uv pip compilenever corrupts an existingrequirements.lock.txt. If the later sync step fails (e.g. a network blip), just runstack upgrade NAMEagain. - Activation is micromamba's job.
stackbuilds shared environments; you enter them withmicromamba activate <name>or run one-offs withmicromamba run -n <name> <command>. - Name collisions. A bare token prefers a profile over a bundle over a
literal package. When a package name collides with one of your profile or
bundle names, force the package with
pkg:<name>. doctor --fixis conservative. It only applies safe repairs (creating missing directories, renaming legacy files, converting.in/.bundlefiles to YAML with a.bakbackup). Anything destructive stays a printed suggestion.- Migrating old configs. If you previously used
.inprofiles,.bundlefiles, or aprofiles.txt,stack doctorwill point at each leftover and tell you what to rename or convert. - The
[tool.uv-stack]table is tool-owned.stack refreshrewrites it wholesale; comments inside that one table are not preserved. uv-stack's own writes leave every other byte ofpyproject.tomlalone — but refresh also runsuv remove/uv addunder the hood, which edit[project.dependencies]just as they would if you ran them yourself. An interrupted refresh or tracked create may leave apendingkey in the table; the next successfulstack refresh(or re-running the tracked create with--force) cleans it up.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file uv_stack-0.5.0-py3-none-any.whl.
File metadata
- Download URL: uv_stack-0.5.0-py3-none-any.whl
- Upload date:
- Size: 126.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7998e0efe6aab716df5097e0af32402222e1e622b7772792ac78fef5de1cca30
|
|
| MD5 |
3c12c3a03dd55d41b91f56ce4d4a9416
|
|
| BLAKE2b-256 |
a0a8fe7000b7bee718f1cdd1d3307fa74e4b5072fea5d33b59333d276de59368
|