Skip to article frontmatterSkip to article content

Pixi: Introduction

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

Pixi is a cross-platform package manager that can manage complex development workflows.

There are two main features:

What does Pixi solve?

Pixi’s goal is to solve fast reproducible developer and deployment workflows. Often written as “it worked on my machine”. Pixi gives the developer all the required tools to create an environment that they can share with colleagues and servers. While being sure there is nothing missing from the environment to run the project.

There are some key focus points to solve this problem.

  1. Reproducibility: Pixi always locks all packages it installs into the environments into a lockfile
  2. Speed: By using modern technologies like Rust and a big focus on optimizations, Pixi is using as much of the machines capabilities to do it’s work as fast as possible.
  3. Virtualization: By separating environments in dedicated folders, users can easily set up, build, and test a project without worrying about their other projects break in the meantime.
  4. cross-platform: With everything Pixi can do it tries to bridge the gaps between the different operating systems, making collaboration easier than before.
  5. cross-language: While Python is well-supported language, Pixi doesn’t stop there, it also focusses on C/C++, CUDA, Rust, Fortran and more.

All of these points are wrapped in a set of main functionalities.

  1. Virtual environment Management: Pixi can create conda environments and activate them on demand.
  2. Package management: Pixi can install/update/upgrade/remove packages from these environments
  3. Task management: Pixi has a cross-platform task runner built-in, allowing users to share the same commands on all platforms.

We’ll dive deeper into these topics later on.

The project workflow

Pixi is designed to be used in a project-based workflow. Tools like poetry, 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, poetry, 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. This command will , by default, add the conda dependency to the pixi.toml or pyproject.toml file, solve the dependencies, write the lockfile and install the package in the environment. e.g. lets 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.2.6,<3"
pytest = ">=8.3.5,<9"

If you want a specific version of a package, you can specify the version in the command.

pixi add numpy==2.2.6 pytest==8.3.5

Or you can make it more specific by using multiple types of specifiers. For the versions you can use the following specifiers:

All of these specifiers can be combined, e.g. numpy>=2.2.6,<3, or numpy~=2.2.6,!=2.2.7.

For the [dependencies] section, Pixi supports the conda MatchSpec format, which includes:

FieldExampleComment
namenumpy = "*"The name of the package, without the version specifier
versionnumpy = ">=2.2.6,<3"
numpy = {version = "==2.2.6"}
The version specification of the package.
buildnumpy = {build = "py39h1234567_0"}The build string of the package
build_numbernumpy = {build_number = 0}The build number of the package
channelnumpy = {channel = "conda-forge"}The channel where to get the package from (must be defined in [workspace])
licensenumpy = {license = "BSD-3-Clause"}The license of the package

PyPI dependencies

Pixi can also install packages from PyPI, it does this through it’s 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.11.5, <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"

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
version: 6
environments:
  default:
    channels:
    - url: https://prefix.dev/conda-forge/
    indexes:
    - https://pypi.org/simple
    packages:
      osx-arm64:
      - conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda
      - pypi: ...
packages:
- conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda
  sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91
  md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab
  depends:
  - __osx >=11.0
  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
1
2
[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 alot 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.

Multiple environments

Pixi can create multiple environments for you, so you can easily switch between them. This is similar to how conda works, but Pixi keeps them specific to the project.

The best way to explain this is to give an example.

[dependencies] # Read this as `[feature.default.dependencies]`
python = ">=3.11,<3.12"
numpy = "*"

[feature.test.dependencies]
pytest = ">=8.3.5,<9"

[feature.format.dependencies]
ruff = "*"

[feature.debug.dependencies]
ipython = "*"

[environments]
# A development environment with all the tools to play with, overwriting the default environment
default = {features = ["test", "debug", "format"], solve-group = "default"}

# A testing environment with only the tools to test, great for CI
test = {features = ["test"], solve-group = "default"}

# A production environment with only the tools to run the code
# The default feature is always included, so you don't need to specify it
prod = {features = [], solve-group = "default"}

# A minimal environment with only the tools to format the code
format = { features = ["format"] , no-default-feature = true }

This will create the following environments:

EnvironmentFeaturesDependenciesSolve Group
defaulttest, format, debug, defaultpython, numpy, pytest, ruffdefault
testtest, defaultpython, numpy, pytestdefault
proddefaultpython, numpydefault
formatformatruffNone
Pixi environments

More information about the features can be found in the documentation.