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)
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.
  • 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 it first)
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 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)

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).

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"). --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`
├── 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
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.4.4-py3-none-any.whl (104.4 kB view details)

Uploaded Python 3

File details

Details for the file uv_stack-0.4.4-py3-none-any.whl.

File metadata

  • Download URL: uv_stack-0.4.4-py3-none-any.whl
  • Upload date:
  • Size: 104.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.13

File hashes

Hashes for uv_stack-0.4.4-py3-none-any.whl
Algorithm Hash digest
SHA256 6199cb72d46ada78a71afd6aa1bea3d09b213278e45f47d372e282fac0845eb1
MD5 82b3f01c569a3576583147c65053be13
BLAKE2b-256 1ab6e25eaefa387f23c1c6d40862993429fc4f0b18c5fbacb8df830ec4ac0b89

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.0

1 file

This release

0.4.4 This release

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