Modern Environment Tools: uv and pixi#
In Week 1 we settled on conda + pip as
the course default: a blank conda environment for isolation and Python-version
control, then pip install -r requirements.txt for fast package installs. We also
previewed two newer tools, uv and pixi. This chapter is the promised closer look.
Both tools fit naturally into this week’s theme. A recurring idea in Week 3 is running
project steps with a single command (the job of a task runner like doit). As we’ll see,
uv and pixi each ship a run subcommand that executes commands inside the project’s
environment—so they blur the line between an environment manager and a lightweight task
runner.
The point of this chapter is not to change what you use for homework. Keep using
conda + pip so everyone’s setup matches. The goal is to make you fluent in the modern
alternatives you’ll increasingly meet in the wild.
Same code, four toolchains#
The in-class examples repository has a
software_environments/
directory that makes the comparison concrete. The exact same script (analyze.py, a tiny
moving-average crossover demo depending on numpy, pandas, and matplotlib) ships in four
subdirectories. Only the tooling around installing and running it changes:
Directory |
Tool |
Manifest |
One-liner to run |
|---|---|---|---|
conda alone |
|
|
|
conda + pip |
|
|
|
|
|
|
|
|
|
|
Clone that repo and follow along—the manifests below are taken directly from it.
uv#
uv is a single, very fast tool, written in Rust, that
replaces pip, venv, pip-tools, and pyenv all at once. One manifest
(pyproject.toml), one lockfile (uv.lock), one command to run.
Installing uv#
uv is a standalone binary—it doesn’t even need an existing Python:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# or, if you already have Python
pip install uv
The core idea: uv run bootstraps everything#
The headline feature is that a single command sets up the entire environment on demand.
In the 03_uv example, you run:
uv run analyze.py
On the first run, uv will: download Python 3.12 if it isn’t already available, create a
.venv/, resolve and install the dependencies, write a uv.lock file, and then run your
script. Subsequent runs are essentially instant because the environment already exists.
If you prefer an explicit two-step flow, it’s:
uv sync # create/update the environment from pyproject.toml + lock
uv run analyze.py # run inside it
The manifest: a standard pyproject.toml#
uv doesn’t invent its own config format—dependencies live in the standard [project]
table of a pyproject.toml:
[project]
name = "finm-env-uv"
version = "0.1.0"
description = "Moving-average crossover demo, managed by uv"
requires-python = ">=3.12"
dependencies = [
"numpy>=2.0",
"pandas>=2.2",
"matplotlib>=3.9",
]
The companion uv.lock file (generated automatically) pins the exact resolved versions
of every package—direct and transitive—so a collaborator who runs uv sync gets a
byte-for-byte identical environment. This is the reproducibility win over a hand-maintained
requirements.txt.
It still speaks requirements.txt#
You don’t have to adopt pyproject.toml to benefit from uv’s speed. uv has a
pip-compatible interface that reads the very same requirements.txt you already use:
uv venv # create a .venv
uv pip install -r requirements.txt # like pip install, but much faster
This is the gentlest on-ramp: drop uv in front of your existing pip workflow and keep
everything else the same.
Handy uv commands#
Command |
Does |
|---|---|
|
Sync the environment (if needed) and run the script |
|
Create/update |
|
Add a dependency (updates |
|
Re-resolve and rewrite |
|
Fast, pip-compatible install into the env |
|
Install and manage a Python version |
Trade-offs#
Pro: Extremely fast; manages Python versions too; standards-based
pyproject.toml; deterministic lockfile.Con: PyPI-only. It does not install from conda channels, so the non-Python binaries that
condapackages (compilers, system libraries, some scientific stacks) may not be available. That’s exactly the gappixifills.
pixi#
pixi gives you a uv-style workflow—one manifest, one lockfile,
an automatic per-project environment—but it resolves packages from the conda
ecosystem (conda-forge) instead of PyPI. So you get conda’s non-Python binaries with
modern speed and ergonomics. (It can also pull from PyPI when you need to.)
Installing pixi#
# macOS / Linux
curl -fsSL https://pixi.sh/install.sh | bash
Note that, like uv, pixi is self-contained: you do not need a separate conda
install for it to resolve conda packages.
The manifest: pixi.toml, with built-in tasks#
Here is the 04_pixi manifest. Notice the [tasks] table at the bottom—this is where
the task-runner flavor comes in:
[project]
name = "finm-env-pixi"
version = "0.1.0"
description = "Moving-average crossover demo, managed by pixi"
channels = ["conda-forge"]
platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"]
[dependencies]
python = "3.12.*"
numpy = ">=2.0"
pandas = ">=2.2"
matplotlib = ">=3.9"
[tasks]
start = "python analyze.py"
To run it:
pixi run start # runs the `start` task defined above
pixi run python analyze.py # or run anything inside the environment
pixi shell # activate an interactive shell in the environment
On the first pixi run, pixi resolves the environment, installs it under .pixi/, and
writes a pixi.lock for exact, cross-platform reproducibility (note the explicit
platforms list).
Handy pixi commands#
Command |
Does |
|---|---|
|
Run the |
|
Run any command inside the environment |
|
Activate an interactive shell |
|
Add a conda dependency (updates manifest + lock) |
|
Add a PyPI dependency |
When would you pick pixi over uv?#
Reach for pixi when you need packages that live in the conda world—non-Python binaries,
or scientific packages that are easier to get from conda-forge than from PyPI. It’s the
modern, lockfile-first successor to the plain conda workflow. If your project is
pure-Python and all your dependencies are on PyPI, uv is usually the simpler, faster pick.
A note on the task-runner connection#
You’ll have noticed that both uv run analyze.py and pixi run start run something inside
the project’s environment—which is conceptually close to what a task runner like doit
does. The difference in scope is worth keeping straight:
uv run/pixi runguarantee a command executes in the correct, reproducible environment, andpixi’s[tasks]can give those commands friendly names.A dedicated task runner like
doit(this week’s main topic) adds dependency tracking: it knows which tasks depend on which files, skips work that’s already up to date, and chains multi-step pipelines together.
In practice they compose well: you might use doit to define the pipeline and uv run doit
or a pixi task to make sure it always runs in the right environment. For this course,
though, keep doit as the task runner and conda + pip as the environment—this chapter
is about recognizing the alternatives, not replacing the stack.
Takeaway#
conda only |
conda + pip |
uv |
pixi |
|
|---|---|---|---|---|
Package source |
conda channels |
conda (Python) + PyPI |
PyPI |
conda channels (+ PyPI) |
Manages Python version |
✅ |
✅ |
✅ |
✅ |
Non-Python binaries |
✅ |
✅ (via conda) |
❌ |
✅ |
Lockfile |
manual |
manual |
|
|
Speed |
slower |
faster installs |
very fast |
very fast |
Run command |
|
|
|
|
Same code, four toolchains. conda is the long-standing data-science default and the only
one here that natively handles non-Python binaries on its own channels; conda + pip is
the pragmatic hybrid this course recommends; uv and pixi are the fast, modern,
lockfile-first tools—uv from the PyPI world, pixi from the conda world. Whatever you
reach for, your requirements.txt and environment.yml remain the portable interchange
formats that tie them all together.