# 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](https://pydevtools.com/handbook/reference/conda-forge.md) and is managed by [pixi](https://pydevtools.com/handbook/reference/pixi.md).

## Prerequisites

Install pixi following the [official installation instructions](https://pixi.sh/latest/getting_started/). No separate Python install is required.

## Create the project

```console
$ 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](https://pixi.sh/latest/getting_started/) 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:

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

The `channels` field tells pixi where to fetch packages. [conda-forge](https://pydevtools.com/handbook/reference/conda-forge.md) 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

```bash
pixi add python pandas matplotlib
```

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

```console
✔ 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:

```toml {filename="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:

```bash
mkdir data
```

Create `data/weather.csv` with this content:

```csv {filename="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`:

```python {filename="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

```bash
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`:

```toml {filename="pixi.toml"}
[tasks]
analyze = "python analyze.py"
```

Now run the analysis with:

```bash
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](https://pydevtools.com/handbook/reference/pytest.md) as a dependency:

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

Create `test_analyze.py`:

```python {filename="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:

```toml {filename="pixi.toml"}
[tasks]
analyze = "python analyze.py"
test = "pytest"
```

```console
$ 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

{{< /filetree/folder >}}
    {{< /filetree/folder >}}
  {{< /filetree/folder >}}
{{< /filetree/container >}}

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

## Next steps

- [Take Over an Existing Conda Environment](https://pydevtools.com/handbook/tutorial/take-over-an-existing-conda-environment.md) to learn how conda works when you inherit a project
- [pixi reference](https://pydevtools.com/handbook/reference/pixi.md) for a full overview of pixi's features
- [When Should I Choose pixi Over uv?](https://pydevtools.com/handbook/explanation/when-should-i-choose-pixi-over-uv.md) for guidance on picking the right tool
- [uv vs pixi vs conda for Scientific Python](https://pydevtools.com/handbook/explanation/uv-vs-pixi-vs-conda-for-scientific-python.md) for a detailed comparison
