Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Pixi: Introduction

Authors
Affiliations
University of Wisconsin-Madison
prefix.dev GmbH
NVIDIA
Pixi banner

Pixi is a cross-platform workspace manager for reproducible software environments and development workflows. For scientific Python developers, a useful mental model is that Pixi combines parts of conda/mamba, conda-lock, uv, and a task runner, while also being able to manage non-Python dependencies such as CUDA, C/C++, Fortran, R, and Rust packages.

Most participants will already have pieces of this workflow: pip or uv for Python packages, conda or mamba for binary scientific packages, and shell scripts or Makefiles for repeatable commands. Pixi’s value is not that these tools cannot install packages, but that one workspace can describe the full environment and workflow reproducibly.

For pip and uv users, Pixi adds first-class support for conda packages, compiled scientific libraries, CUDA libraries, compilers, and multi-platform lock files. For conda and mamba users, Pixi keeps the conda package ecosystem while adding project-local lock files, tasks, named environments, and rich platform declarations.

There are two main workflows:

What does Pixi solve?

Pixi helps make the answer to “what did you run, and where?” explicit and reproducible. For this tutorial, the important pieces are:

  1. Reproducibility by default: Pixi writes a pixi.lock file with the exact packages for each environment and platform.

  2. Conda and PyPI together: Pixi can install conda packages and PyPI packages into the same environment, using uv for PyPI dependencies.

  3. Project-local environments: Environments live with the workspace, so collaborators do not need to recreate your local conda environment naming scheme.

  4. Cross-Platform and Cross-Language workflows: Pixi can describe Linux, macOS, Windows, CUDA, Python, C/C++, Fortran, Rust, R, and more in one project model.

  5. Tasks: Pixi provides a cross-platform task runner so commands like testing, training, building, and linting are part of the shared workspace.

  6. Package building: Pixi can build conda packages from source, including CUDA-enabled packages, and publish them to a channel for others to use.

For common workflow steps, Pixi can replace a few commands you might otherwise reach for from different tools:

TaskYou might usePixi
Add a conda packageconda install scipy / mamba install scipypixi add scipy
Add a PyPI packagepip install pydantic / uv add pydanticpixi add --pypi pydantic
Run a command in the environmentconda run ... / uv run ...pixi run ...
Start an activated shellconda activate my-envpixi shell
Lock an environmentconda-lock / uv lockpixi lock
Run project commandsmake test / shell scriptspixi run test
Install a global CLI toolpipx install ruff / uv tool install ruffpixi global install ruff

The project workflow

Pixi is designed to be used in a project-based workflow. Tools like uv, npm, deno, cargo, maven and pixi are all designed to be used in a project-based workflow. This means that you can create a project and then use Pixi to manage the dependencies and tasks for that project. You can think of a project as a self-contained directory that contains all the files and configurations needed to build and run your application. Often the project will keep the environment it installs close to the project folder itself, so it will not clutter the system. This is a great way to keep your projects organized and to avoid conflicts between different projects.

Project-based vs Environment-based vs System-based

To give a little background why Pixi is designed this way, let’s take a look at the different ways to manage packages and environments.

Project-based workflow
Environment-based workflow
System-based workflow

Supporting tools: pixi, uv, npm, deno, cargo, maven

Pros:

  • Isolated environments per project (no conflicts)

  • Easy to reproduce and share with others (declarative)

  • Keeps dependencies close to the project

Cons:

  • Potentially use more disk space (multiple environments)

  • Managing environments over multiple projects is less straightforward.

Creating a project

As Pixi uses the project-based workflow, it uses a manifest file to keep track of the dependencies and tasks for the project. This is also known as declarative configuration, where you describe what you want, and Pixi will take care of the rest. The manifest file is called pixi.toml, or you can use pyproject.toml, and it is located in the root of the project.

To create a new project, you can use the pixi init command.

pixi.toml
pyproject.toml
pixi init my_project

This will create a new directory called my_project and initialize a new pixi.toml file in it.

my_project
├── .gitattributes
├── .gitignore
└── pixi.toml

The pixi.toml file is a TOML file that contains the configuration for the project.

pixi.toml
1
2
3
4
5
6
7
8
9
10
[workspace]
authors = ["Jane Doe <jane.doe@example.com>"]
channels = ["https://prefix.dev/conda-forge"]
name = "my_project"
platforms = ["osx-arm64"]
version = "0.1.0"

[tasks]

[dependencies]

The pixi.toml doesn’t have the basic Python package structure like the pyproject.toml file, because it is not a Python package by default.

As pixi.toml has a JSON schema, it is possible to use IDE’s like VSCode to edit the field with autocompletion. Install the Even Better TOML VSCode extension to get the best experience. Or use the integrated schema support in PyCharm.

For the rest of this tutorial, we will use the pixi.toml file as the main file.

Managing dependencies

After creating the project, you can start adding dependencies to the project. Pixi uses the pixi add command to add dependencies to the project. By default, this command adds the conda dependency to the pixi.toml or pyproject.toml file, solves the dependencies, writes the lockfile, and installs the package in the environment. For example, let’s add numpy and pytest to the project.

pixi add numpy pytest

This will result in the following manifest file:

pixi.toml
1
2
3
4
5
6
7
8
9
10
11
12
[workspace]
authors = ["Jane Doe <jane.doe@example.com>"]
channels = ["https://prefix.dev/conda-forge"]
name = "my_project"
platforms = ["osx-arm64"]
version = "0.1.0"

[tasks]

[dependencies]
numpy = ">=2.5.1,<3"
pytest = ">=9.1.1,<10"

If you want a specific version or range, provide it when adding the package. The most common forms are:

pixi add "numpy==2.2.6"
pixi add "numpy>=2.2,<3"
pixi add "python=3.12.*"
pixi add conda-forge::numpy

Pixi supports conda MatchSpecs, so you can be more specific when needed, but most projects only need package names, version ranges, and occasionally a channel-qualified dependency.

PyPI dependencies

Pixi can also install packages from PyPI through its integration with uv. In the Rust code, Pixi depends on the uv package manager to install the packages from PyPI. This means that you can use the pixi add --pypi command to install packages from PyPI.

pixi add --pypi pydantic

Which results in it being added to the manifest file as:

pixi.toml
pyproject.toml

In the pixi.toml file, it will be added to the [pypi-dependencies] section.

pixi.toml
1
2
[pypi-dependencies]
pydantic = ">=2.13.4, <3"

What pixi does differently from managing PyPI packages through other package managers, is that it will install the packages in the same environment as the conda packages, but will not overwrite the conda packages. We’ve got a mapping between the conda packages and the PyPI packages, so that we can let uv know which packages to install and which packages to ignore because they are already installed.

Special types of dependencies

Pixi has a few special types of dependencies that you can use in the project.

TypeDescriptionExample
gitInstall a package from a git repositorygit = "https://github.com/user/repo.git"
branchInstall a specific branch from a git repository (requires git)branch = "main"
tagInstall a specific tag from a git repository (requires git)tag = "v1.0.0"
revInstall a specific commit from a git repository (requires git)rev = "abc123"
pathInstall a package from a local directorypath = "./local-python-package"
editable (pypi only)Install a package in editable modeeditable = true
urlInstall a package from a URLurl = "https://example.com/package.whl"

An example of what this might look like in the Pixi manifest is

pixi.toml
# Git repository of a Pixi package
[dependencies.git-package]
# Git repository
git = "https://github.com/org/repo"
# Git branch
branch = "main"
# Subdirectory within repo
subdirectory = "packages/mypackage"

[dependencies.tagged-git-package]
# Git with specific tag
git = "https://github.com/org/repo"
tag = "v1.0.0"

[dependencies.rev-git-package]
# Git with specific revision
git = "https://github.com/org/repo"
rev = "abc123def"

Lockfile

The lockfile is a file that contains the exact versions of the packages that were installed in the environment. This file is used to ensure that the same versions of the packages are installed in the environment when the project is shared with others. What should you know about the lockfile?

pixi.lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: 7
platforms:
- name: linux-64
  virtual-packages:
  - __unix=0=0
  - __linux=4.18
  - __glibc=2.28
  - __archspec=0=x86_64
environments:
  default:
    channels:
    - url: https://prefix.dev/conda-forge/
    indexes:
    - https://pypi.org/simple
    packages:
      linux-64:
      - conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-h99b78c6_7.conda
      - pypi: ...
packages:
- conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-h99b78c6_7.conda
  sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91
  md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab
  depends:
  - __unix
  license: bzip2-1.0.6
  license_family: BSD
  size: 122909
  timestamp: 1720974522888
- pypi: ...

Example lockfile, highly simplified for readability

Managing tasks

Pixi has a built-in cross-platform task runner that allows you to define tasks in the manifest. This is a great way to share tasks with others and to ensure that the same tasks are run in the same environment. The tasks are defined in the [tasks] section.

Basic tasks

You can use the pixi task command to modify the tasks in the project.

pixi task add hello "echo Hello World"

This will add a new task called hello to the project, which will print Hello World to the console.

pixi.toml
[tasks]
hello = "echo Hello World"

You can also use the pixi run command to run the tasks in the project.

pixi run hello

This will run the hello task and print Hello World to the console.

Environments

Now you know the basics of dealing with the Pixi manifest basics. Next step is actually use the environments it can create for you.

Activating environments

Because Pixi creates virtual environments for you, it is important to activate the environment before running any commands. You can do this by using the pixi shell or the pixi run command, these commands will automatically activate the environment for you.

pixi run python -VV
# or:
pixi shell
python -VV
exit

Activating an environment is not much more than running a script that sets the environment variables for you. To investigate this, you can use pixi shell-hook to view what the shell script looks like.

pixi shell-hook

This will print the shell script that is used to activate the environment.

Platforms

Pixi solves environments for the platforms listed in the workspace manifest. A typical scientific Python project might support macOS laptops, Windows laptops, and Linux servers from one file:

pixi.toml
1
2
3
[workspace]
channels = ["conda-forge"]
platforms = ["osx-arm64", "linux-64", "win-64"]

Pixi can also use richer platform declarations. For example, later in this tutorial we will distinguish a regular Linux target from a CUDA-capable Linux target:

pixi.toml
1
2
3
4
5
6
7
8
[workspace]
channels = ["conda-forge"]
platforms = [
  "osx-arm64",
  "win-64",
  "linux-64",
  { name = "linux-64-cuda", platform = "linux-64", cuda = "12" },
]

This lets one workspace describe both local CPU development and remote GPU execution with the platform requirements for each made explicit.

Features and environments

Pixi separates reusable configuration from the environments you actually run.

A feature is a named block of configuration. It can define dependencies, tasks, platforms, channels, or activation settings.

An environment is a named combination of features. It is what Pixi solves, installs, and runs.

The top-level [dependencies] and [tasks] tables are the default feature. The default feature is included automatically unless no-default-feature = true is set.

pixi.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[workspace]
channels = ["conda-forge"]
platforms = ["linux-64", "osx-arm64", "win-64"]

# Default feature: shared runtime dependencies.
[dependencies]
python = ">=3.14.6,<3.15"
numpy = ">=2.5.1,<3"

[tasks]
run-example = "python -c 'import numpy; print(numpy.__version__)'"

[feature.notebook.dependencies]
jupyterlab = ">=4.6.1,<5"

[feature.notebook.tasks]
notebook = "jupyter lab"

[feature.docs.dependencies]
mystmd = ">=1.10.1,<2"

[feature.docs.tasks]
docs = "myst build --html"

[environments]
default = { features = [] }
notebook = { features = ["notebook"] }

# Documentation can be independent of the runtime dependencies.
docs = { features = ["docs"], no-default-feature = true }

This gives you three environments:

Environment you runFeatures combined into itExample command
defaultdefaultpixi run run-example
notebookdefault + notebookpixi run notebook
docsdocs onlypixi run docs

Pixi can run notebook and docs without -e because each task is available in only one environment. Use -e or --environment when you want to select an environment explicitly.

Use a feature to define a reusable workflow layer. Use an environment to choose which layers Pixi should install and run. Use no-default-feature = true when an environment should not include the shared runtime dependencies.

Building packages

So far, we have used Pixi to manage environments and tasks. Pixi can also build conda packages from source through build backends. In practice, anything that can be packaged as a conda package can be built this way with a matching backend, including Python packages, C/C++ or Fortran libraries, and internal libraries that are not already packaged.

Pixi Build is currently a preview feature, enabled in the workspace manifest:

pixi.toml
1
2
3
4
[workspace]
preview = ["pixi-build"]
channels = ["conda-forge"]
platforms = ["linux-64"]

A workspace can then depend on a local source package:

pixi.toml
[dependencies]
my-library = { path = "src/my-library" }

If my-library contains Pixi package metadata, Pixi can build it as a conda package and install the built package into the environment. We will return to this in a later chapter.