Skip to main content

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 with uv (fast), and verified with uv pip check.

Installation

Prerequisites (both must be installed and reachable):

  • uv
  • micromamba — run micromamba shell init after installing so $MAMBA_EXE is 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 like cpython@3.12 is passed straight to uv.
  • Anything else is treated as a micromamba environment name — the project uses that environment's interpreter. stack create project ds --python main builds the project on main'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 called 3.12.x. stack edit project warns about one in the tracking table when the editor exits. (Note that 3.12.* is legal in an environment's python.txt, which conda reads, not uv.)
  • With no --python, the default comes from $UV_STACK_PROJECT_PYTHON, then <config-root>/project-python.txt, then 3.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:

  1. --editor
  2. $UV_STACK_EDITOR
  3. <config-root>/editor.txt
  4. $VISUAL
  5. $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:

  1. --root PATH (global flag)
  2. $UV_STACK_ROOT
  3. $UV_ENV_ROOT (the historical spelling, still accepted)
  4. ~/.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, and requirements.lock.txt are overwritten on every upgrade. For env-specific additions use requirements.local.in; for anything reusable, a profile.
  • --dry-run is almost dry. It runs no commands and never touches the lock or the environment, but it does re-render requirements.in and environment.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 compile never corrupts an existing requirements.lock.txt. If the later sync step fails (e.g. a network blip), just run stack upgrade NAME again.
  • Activation is micromamba's job. stack builds shared environments; you enter them with micromamba activate <name> or run one-offs with micromamba 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 --fix is conservative. It only applies safe repairs (creating missing directories, renaming legacy files, converting .in/.bundle files to YAML with a .bak backup). Anything destructive stays a printed suggestion.
  • Migrating old configs. If you previously used .in profiles, .bundle files, or a profiles.txt, stack doctor will point at each leftover and tell you what to rename or convert.
  • The [tool.uv-stack] table is tool-owned. stack refresh rewrites it wholesale; comments inside that one table are not preserved. uv-stack's own writes leave every other byte of pyproject.toml alone — but refresh also runs uv remove/uv add under the hood, which edit [project.dependencies] just as they would if you ran them yourself. An interrupted refresh or tracked create may leave a pending key in the table; the next successful stack 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

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

uv_stack-0.5.0-py3-none-any.whl (126.6 kB view details)

Uploaded Python 3

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

Hashes for uv_stack-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7998e0efe6aab716df5097e0af32402222e1e622b7772792ac78fef5de1cca30
MD5 3c12c3a03dd55d41b91f56ce4d4a9416
BLAKE2b-256 a0a8fe7000b7bee718f1cdd1d3307fa74e4b5072fea5d33b59333d276de59368

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.0 This release

1 file

0.4.4

1 file

0.1.6

1 file

0.1.5

1 file

0.1.4

1 file

0.1.3

1 file

0.1.2

1 file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page