Skip to content

Create your first Python project with pixi

This tutorial builds a weather data analysis tool that reads CSV data, computes statistics with pandas, and produces a chart with matplotlib. Every dependency comes from conda-forge and is managed by pixi.

Prerequisites

Install pixi following the official installation instructions. No separate Python install is required.

Create the project

$ pixi init weather_analysis
✔ Created /path/to/weather_analysis/pixi.toml
$ cd weather_analysis

If you see pixi: command not found, restart your shell so the installer’s PATH update takes effect, or follow the official installation instructions again.

Notice the .gitignore and .gitattributes files pixi created alongside pixi.toml. They come pre-configured so the project-local .pixi/ environment never lands in version control.

pixi.toml stores project metadata, dependencies, and task definitions:

pixi.toml
[workspace]
name = "weather_analysis"
channels = ["conda-forge"]
platforms = ["osx-arm64"]

The channels field tells pixi where to fetch packages. conda-forge is a community-maintained, openly licensed package repository with thousands of scientific Python packages.

The platforms value reflects your current machine. Pixi sets this automatically.

Note

You may also see a pixi.lock file. This lockfile pins the exact version of every package pixi installs. Commit it to version control so collaborators reproduce the same environment.

Add dependencies

pixi add python pandas matplotlib

pixi prints one confirmation line per package as it resolves and installs:

✔ Added python >=3.14.4,<3.15
✔ Added pandas >=3.0.2,<4
✔ Added matplotlib >=3.10.9,<4

If pixi add fails with Failed to fetch repodata, pixi could not reach conda-forge. Check your internet connection and rerun the command.

This does three things:

  1. Resolves compatible versions of Python, pandas, matplotlib, and all their transitive dependencies from conda-forge.
  2. Installs everything into a .pixi/ directory inside the project (a project-local environment, not a global one).
  3. Updates pixi.lock with the exact versions.

Notice the new .pixi/ directory and pixi.lock file in your project. .pixi/ holds the local environment (Python interpreter, every installed package). pixi.lock records exact resolved versions so a teammate running pixi install against the same lockfile gets the same environment.

The pixi.toml now includes:

pixi.toml
[dependencies]
python = ">=3.13.3,<4"
pandas = ">=2.2.3,<3"
matplotlib = ">=3.10.1,<4"

The version bounds reflect whichever releases are current when you run the command.

Tip

The .pixi/ directory contains the full environment (Python interpreter, all packages). It can be large. Add .pixi/ to .gitignore and let collaborators recreate it with pixi install.

Create sample data

Create a data directory and add a CSV file:

mkdir data

Create data/weather.csv with this content:

data/weather.csv
date,city,temp_high,temp_low,precipitation_mm,humidity_pct
2025-01-01,Portland,8,2,12.5,82
2025-01-02,Portland,7,1,0.0,65
2025-01-03,Portland,9,3,8.3,78
2025-01-04,Portland,6,-1,15.2,88
2025-01-05,Portland,10,4,0.0,60
2025-01-01,Phoenix,18,5,0.0,25
2025-01-02,Phoenix,20,7,0.0,22
2025-01-03,Phoenix,22,8,0.0,20
2025-01-04,Phoenix,19,6,2.1,35
2025-01-05,Phoenix,21,7,0.0,23

Write the analysis script

Create analyze.py:

analyze.py
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
from pathlib import Path

matplotlib.use("Agg")


def load_weather_data(path):
    df = pd.read_csv(path, parse_dates=["date"])
    df["temp_range"] = df["temp_high"] - df["temp_low"]
    return df


def summarize_by_city(df):
    return df.groupby("city").agg(
        avg_high=("temp_high", "mean"),
        avg_low=("temp_low", "mean"),
        total_precip=("precipitation_mm", "sum"),
        avg_humidity=("humidity_pct", "mean"),
    ).round(1)


def plot_temperature_comparison(df, output_path):
    fig, ax = plt.subplots(figsize=(8, 4))
    for city, group in df.groupby("city"):
        ax.plot(group["date"], group["temp_high"], marker="o", label=f"{city} high")
        ax.plot(group["date"], group["temp_low"], marker="s", label=f"{city} low",
                linestyle="--", alpha=0.6)
    ax.set_ylabel("Temperature (°C)")
    ax.set_title("Daily Temperatures by City")
    ax.legend()
    fig.tight_layout()
    fig.savefig(output_path, dpi=150)
    print(f"Chart saved to {output_path}")
    plt.close(fig)


def main():
    data_dir = Path(__file__).parent / "data"
    df = load_weather_data(data_dir / "weather.csv")

    summary = summarize_by_city(df)
    print("Weather Summary by City:")
    print(summary)
    print()

    plot_temperature_comparison(df, data_dir / "temperatures.png")


if __name__ == "__main__":
    main()

Run the script

pixi run python analyze.py

Expected output:

Weather Summary by City:
         avg_high  avg_low  total_precip  avg_humidity
city
Phoenix      20.0      6.6           2.1          25.0
Portland      8.0      1.8          36.0          74.6

Chart saved to data/temperatures.png

If you see error: failed to find pyproject.toml or pixi.toml, you ran the command outside the project directory. cd weather_analysis and try again.

Notice the new data/temperatures.png file. Open it to confirm the chart rendered. The matplotlib.use("Agg") line near the top of analyze.py is what tells matplotlib to write the image to disk rather than open a GUI window.

pixi run activates the project environment and executes the command. You never need to activate or deactivate environments manually.

Add a named task

Instead of typing pixi run python analyze.py every time, define a task in pixi.toml:

pixi.toml
[tasks]
analyze = "python analyze.py"

Now run the analysis with:

pixi run analyze

The output is identical to running pixi run python analyze.py directly. The named task is just an alias for that command.

Tasks are useful for longer commands, pipelines, and giving collaborators discoverable entry points into the project.

Add a dev dependency and write a test

Add pytest as a dependency:

$ pixi add pytest
✔ Added pytest >=9.0.3,<10

Create test_analyze.py:

test_analyze.py
import pandas as pd
from analyze import load_weather_data, summarize_by_city
from pathlib import Path


def test_load_weather_data():
    path = Path(__file__).parent / "data" / "weather.csv"
    df = load_weather_data(path)
    assert "temp_range" in df.columns
    assert len(df) == 10


def test_summarize_by_city():
    df = pd.DataFrame({
        "city": ["A", "A", "B"],
        "temp_high": [20, 22, 10],
        "temp_low": [10, 12, 5],
        "precipitation_mm": [0.0, 5.0, 10.0],
        "humidity_pct": [50, 60, 70],
    })
    summary = summarize_by_city(df)
    assert summary.loc["A", "avg_high"] == 21.0
    assert summary.loc["B", "total_precip"] == 10.0

Add a test task and run it:

pixi.toml
[tasks]
analyze = "python analyze.py"
test = "pytest"
$ pixi run test
============================= test session starts ==============================
platform darwin -- Python 3.14.4, pytest-9.0.3, pluggy-1.6.0
rootdir: /path/to/weather_analysis
collected 2 items

test_analyze.py ..                                                       [100%]

============================== 2 passed in 0.33s ===============================

The two dots after test_analyze.py are one dot per passing test. If you see F or E instead, rerun with pixi run pytest -v to see the failing assertion.

Final project structure

    • pixi.toml
    • pixi.lock
    • analyze.py
    • test_analyze.py
    • .gitignore
      • weather.csv

Commit pixi.toml, pixi.lock, your source files, and data. The .pixi/ directory is local and should stay in .gitignore.

Next steps

Last updated on