Learn how to use pyproject.toml as your Pixi manifest file for Python projects, combining standard Python packaging with Pixi’s powerful environment management.
Why pyproject.toml?
Pixi supports both pixi.toml and pyproject.toml manifest formats. For Python projects, pyproject.toml is recommended because:
It’s the standard format for Python projects
It integrates with existing Python tooling
It combines project metadata with Pixi configuration
Other Python developers will be familiar with it
For non-Python projects, use pixi.toml instead.
Initial Setup
Initialize a project with pyproject.toml:
pixi init my-project --format pyproject
cd my-project
Convert an Existing Project
If you already have a pyproject.toml, initialize Pixi in that directory:
Add a [tool.pixi.workspace] section
Add your project as an editable PyPI dependency
Update .gitignore and .gitattributes
Project Structure
A typical project structure looks like:
my-project/
├── pyproject.toml
├── src/
│ └── my_project/
│ └── __init__.py
├── tests/
│ └── test_my_project.py
├── .pixi/
│ └── envs/
│ └── default/
└── pixi.lock
Understanding pyproject.toml
The [project] section defines standard Python metadata:
[ project ]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.11"
description = "My awesome project"
authors = [
{ name = "Your Name" , email = "you@example.com" }
]
dependencies = []
Pixi Workspace Configuration
Pixi configuration lives under [tool.pixi]:
[ tool . pixi . workspace ]
channels = [ "conda-forge" ]
platforms = [ "linux-64" , "osx-64" , "osx-arm64" , "win-64" ]
[ tool . pixi . pypi-dependencies ]
my-project = { path = "." , editable = true }
Build System
Define how to build your package:
[ build-system ]
requires = [ "hatchling" ]
build-backend = "hatchling.build"
Pixi uses this when installing your package as an editable dependency. If omitted, it defaults to setuptools.
Python Version Management
Automatic Python Installation
The requires-python field automatically manages Python:
[ project ]
requires-python = ">=3.11"
This is equivalent to in pixi.toml:
[ dependencies ]
python = ">=3.11"
Pixi automatically installs the appropriate Python interpreter - no system installation needed!
Version Constraints
Minimum Version
Version Range
Exact Version
[ project ]
requires-python = ">=3.11"
Dependency Management
PyPI Dependencies
Dependencies in the [project] section become PyPI dependencies:
[ project ]
dependencies = [
"numpy" ,
"pandas>=2.0" ,
"matplotlib>=3.8,<4" ,
]
Add dependencies using:
pixi add --pypi numpy pandas matplotlib
Conda Dependencies
Use [tool.pixi.dependencies] for conda packages:
[ tool . pixi . dependencies ]
opencv = ">=4.9.0,<5"
mkl = ">=2024.0.0,<2025"
compilers = "*"
Add conda dependencies:
pixi add opencv mkl compilers
Mixing Both Sources
Pixi seamlessly handles packages from both sources:
[ project ]
dependencies = [
"requests" ,
"httpx" ,
]
[ tool . pixi . dependencies ]
numpy = ">=2.0.0,<3"
matplotlib = ">=3.8.0,<4"
# Add PyPI packages
pixi add --pypi requests httpx
# Add conda packages
pixi add numpy matplotlib
Priority Rules
When the same package is in both sections:
[ project ]
dependencies = [ "numpy" ] # PyPI version
[ tool . pixi . dependencies ]
numpy = ">=2.0.0,<3" # Conda version - this wins!
Conda dependencies always take precedence over PyPI dependencies.
Optional Dependencies and Features
Optional Dependencies (Legacy)
Define optional dependency groups:
[ project . optional-dependencies ]
test = [ "pytest" , "pytest-cov" ]
docs = [ "sphinx" , "sphinx-rtd-theme" ]
dev = [ "ruff" , "mypy" ]
Pixi automatically converts these to features:
Result:
[ tool . pixi . environments ]
default = { features = [], solve-group = "default" }
test = { features = [ "test" ], solve-group = "default" }
docs = { features = [ "docs" ], solve-group = "default" }
dev = { features = [ "dev" ], solve-group = "default" }
Dependency Groups (PEP 735)
The modern approach using dependency groups:
[ dependency-groups ]
test = [ "pytest" , "pytest-cov" ]
docs = [ "sphinx" ]
dev = [
{ include-group = "test" },
{ include-group = "docs" },
"ruff" ,
"mypy" ,
]
Add to a dependency group:
pixi add --pypi --feature test pytest pytest-cov
Self-References
Dependency groups can reference each other:
[ project . optional-dependencies ]
test = [ "pytest" ]
all = [ "package2" , "my_project[test]" ]
Environment Configuration
Single Environment
By default, Pixi creates one environment:
[ tool . pixi . workspace ]
channels = [ "conda-forge" ]
platforms = [ "linux-64" ]
[ tool . pixi . dependencies ]
numpy = "*"
[ tool . pixi . pypi-dependencies ]
my-project = { path = "." , editable = true }
Multiple Environments
Create separate environments for different purposes:
[ tool . pixi . environments ]
default = { solve-group = "default" }
test = { features = [ "test" ], solve-group = "default" }
prod = { features = [ "prod" ], solve-group = "default" , no-default-feature = true }
Add environments:
pixi workspace environment add test --feature test --solve-group default
pixi workspace environment add prod --no-default-feature
Solve Groups
Solve groups ensure dependency versions are consistent across environments:
[ tool . pixi . environments ]
default = { solve-group = "default" }
test = { features = [ "test" ], solve-group = "default" }
Environments in the same solve group share the same dependency versions, ensuring consistency between development and testing.
Tasks
Define Tasks
Create reusable commands:
[ tool . pixi . tasks ]
start = "python -m my_project"
lint = "ruff check ."
format = "ruff format ."
test = "pytest tests/"
Run tasks:
pixi run test
pixi run lint
pixi run format
Feature-Specific Tasks
Tasks can be scoped to features:
[ tool . pixi . feature . test . tasks ]
test = "pytest"
coverage = "pytest --cov=my_project tests/"
Task Dependencies
Tasks can depend on other tasks:
[ tool . pixi . tasks ]
check = { depends-on = [ "lint" , "test" ] }
lint = "ruff check ."
test = "pytest"
For monorepo setups, use [tool.uv.sources] to reference local packages:
Project Structure
workspace/
├── main_project/
│ └── pyproject.toml
├── package_a/
│ └── pyproject.toml
└── package_b/
└── pyproject.toml
Main Project Configuration
# main_project/pyproject.toml
[ tool . pixi . pypi-dependencies ]
package-a = { path = "../package_a" }
Package A Configuration
# package_a/pyproject.toml
[ project ]
name = "package-a"
dependencies = [ "flask" , "package-b" ]
[ tool . uv . sources ]
flask = { git = "https://github.com/pallets/flask" , branch = "main" }
package-b = { path = "../package_b" }
[tool.uv.sources] only works in dependencies, not in the main manifest.
Build System Options
Hatchling (Recommended)
[ build-system ]
requires = [ "hatchling" ]
build-backend = "hatchling.build"
Benefits:
Modern and fast
Minimal configuration
Good defaults for most projects
[ build-system ]
requires = [ "setuptools>=40.8.0" ]
build-backend = "setuptools.build_meta:__legacy__"
Poetry
[ build-system ]
requires = [ "poetry-core" ]
build-backend = "poetry.core.masonry.api"
Complete Example
Here’s a complete pyproject.toml for a production project:
[ project ]
name = "my-app"
version = "0.1.0"
requires-python = ">=3.11"
description = "A production-ready application"
authors = [
{ name = "Your Name" , email = "you@example.com" }
]
classifiers = [
"Programming Language :: Python :: 3" ,
"Programming Language :: Python :: 3.11" ,
"Programming Language :: Python :: 3.12" ,
]
dependencies = [
"flask>=3.0.0" ,
"requests>=2.31.0" ,
]
[ dependency-groups ]
test = [ "pytest" , "pytest-cov" ]
dev = [{ include-group = "test" }, "ruff" , "mypy" ]
[ build-system ]
requires = [ "hatchling" ]
build-backend = "hatchling.build"
[ tool . pixi . workspace ]
channels = [ "conda-forge" ]
platforms = [ "linux-64" , "osx-64" , "osx-arm64" , "win-64" ]
[ tool . pixi . dependencies ]
gunicorn = ">=21.2.0,<22"
numpy = ">=2.0.0,<3"
[ tool . pixi . pypi-dependencies ]
my-app = { path = "." , editable = true }
[ tool . pixi . feature . dev . pypi-dependencies ]
my-app = { path = "." , editable = true }
[ tool . pixi . feature . prod . pypi-dependencies ]
my-app = { path = "." , editable = false }
[ tool . pixi . tasks ]
dev = "flask --app my_app:app run --debug"
start = "gunicorn -w 4 my_app:app --bind :8000"
lint = "ruff check ."
format = "ruff format ."
[ tool . pixi . feature . test . tasks ]
test = "pytest"
coverage = "pytest --cov=my_app tests/"
[ tool . pixi . environments ]
default = { features = [ "dev" ], solve-group = "default" }
test = { features = [ "test" ], solve-group = "default" }
prod = { features = [ "prod" ], solve-group = "default" }
[ tool . pixi . system-requirements ]
macos = "12.0" # Required for some dependencies
Best Practices
Always include a build-system section:
Even if you’re not distributing your package, it’s required for editable installs.
Use dependency groups over optional-dependencies:
The PEP 735 dependency groups are more flexible and modern.
Prefer conda for system dependencies:
Use conda for packages with C/C++ dependencies, PyPI for pure Python packages.
Use solve groups for consistency:
Put all environments in the same solve group to ensure version consistency.
Comparison with pixi.toml
pyproject.toml
pixi.toml (equivalent)
[ project ]
name = "my-project"
requires-python = ">=3.11"
dependencies = [ "numpy" ]
[ tool . pixi . workspace ]
channels = [ "conda-forge" ]
platforms = [ "linux-64" ]
[ tool . pixi . dependencies ]
python = ">=3.11"
Next Steps
Troubleshooting
Build Backend Not Found
If you see “build backend not found”, add a [build-system] section:
[ build-system ]
requires = [ "hatchling" ]
build-backend = "hatchling.build"
Editable Install Issues
Ensure your project is in [tool.pixi.pypi-dependencies]:
[ tool . pixi . pypi-dependencies ]
my-project = { path = "." , editable = true }
Mixed Dependency Sources
If a package is in both [project.dependencies] and [tool.pixi.dependencies], the conda version takes precedence.