---
name: uv-ecosystem
type: reference
title: "uv Ecosystem — Package Manager Best Practices"
description: "Entrypoints, tool isolation, packaging hygiene, and anti-patterns for Python projects managed with uv. Distilled from Pokemon TCG MLX migration and other projects."
tags: [uv, python, packaging, entrypoints, best-practices, tooling]
timestamp: 2026-07-25
---

# uv Ecosystem — Best Practices

Dense reference for Python packaging and tooling via uv. Every rule here was learned the hard way.

## The Golden Rule

If you are writing `pip install`, `python script.py`, or `requirements.txt` — **stop**. Use `uv`.

If you are writing `PYTHONPATH=.` — **stop**. Fix the project structure instead.

Derived from [[preferencias-tecnicas]] (the directive that mandates uv). Crystallized during the [[pokemon_tcg_mlx_migration]] project.

## Core Principles

### 1. `uv run` Is the Universal Runner

Never invoke `python` or `python3` directly. Always route through uv.

```bash
# Bad
python script.py
python3 -m scripts.bc.bc_train

# Good
uv run python script.py
uv run --env-file .env script.py
uv run my-command
uv run --with ruff ruff check .
```

uv resolves the venv, Python version, and dependencies from `pyproject.toml` automatically. Direct `python` bypasses all of that.

### 2. `[project.scripts]` Entry Points Over PYTHONPATH

`PYTHONPATH=.` is a code smell. It means the project structure is wrong. Fix it with proper packaging.

In `pyproject.toml`:

```toml
[project.scripts]
bc-train = "scripts.bc.bc_train:main"
evaluate = "scripts.evaluate:main"
build-dataset = "scripts.bc.build_bc_dataset:main"
```

Then:

```bash
uv run bc-train data/bc_data/bc_2026_07_21 --d-model 128
uv run evaluate -n 50
```

This works from **any directory**, no path hacking required. After editing `pyproject.toml`, always run `uv sync` to register the entry points.

### 3. `uv tool install` for Standalone CLI Tools

Global CLI tools (like `kaggle`, `ruff`, `gh`) should be installed via `uv tool install`, not `pip install`. This isolates them from the project venv.

```bash
uv tool install kaggle
uv tool install ruff
```

Each tool gets its own isolated environment. No venv pollution, no version conflicts.

### 4. `uvx` for One-Off Tool Runs

Run a tool without installing it permanently:

```bash
uvx ruff check .
uvx mypy src/
uvx pytest tests/
```

Equivalent to `uv run --with <tool> <tool>` but shorter. Use for tools you do not need continuously.

### 5. `uv add` vs `uv pip install`

| Operation | Command | Effect |
|---|---|---|
| Project dependency | `uv add numpy` | Modifies `pyproject.toml` + locks |
| Dev dependency | `uv add --dev pytest` | Adds to `[dependency-groups]` |
| Temp install | `uv pip install numpy` | Installed in venv only, no lock change |
| Install from lock | `uv sync` | Installs everything from lockfile |

Rule: for project dependencies, **always `uv add`**. `uv pip install` is for temporary experiments only.

### 6. `uv sync` After Changing pyproject.toml

Entry points, new deps, and version changes only take effect after:

```bash
uv sync
```

This is the step people forget. If `uv run my-command` says "command not found", you forgot to sync.

## Anti-Patterns

### PYTHONPATH Hacks

```bash
# BAD — this means the project is not a proper Python package
PYTHONPATH=. python scripts/bc/bc_train.py
```

**Fix chain:**
1. Add `__init__.py` to every importable directory (`scripts/`, `scripts/bc/`, `rl/`, etc.)
2. Add `[project.scripts]` entry point in `pyproject.toml`
3. Use relative imports within the package
4. Run via `uv run <entrypoint>`

**Legitimate exception:** Kaggle sandbox where `__file__` is not defined. That is a runtime constraint, not a hack.

### sys.path.insert(0, ...)

```python
# BAD
import sys
sys.path.insert(0, os.path.dirname(__file__))
```

**Same fix chain as PYTHONPATH.** If you see this in code, the package structure is broken.

### Direct python/python3 Invocation

```bash
# BAD
python3 -u scripts/bc/bc_train.py data/bc_data/bc_2026_07_21

# GOOD
uv run python -u scripts/bc/bc_train.py data/bc_data/bc_2026_07_21
```

Direct invocation bypasses the uv-managed venv and Python version.

## Package Structure Checklist

Every Python directory that contains importable modules must have `__init__.py`:

```
project/
├── pyproject.toml
├── src/
│   └── mypackage/
│       ├── __init__.py
│       ├── module_a.py
│       └── subpackage/
│           ├── __init__.py
│           └── module_b.py
└── scripts/
    ├── __init__.py      # if scripts are importable
    └── bc/
        ├── __init__.py
        └── bc_train.py
```

Directories without `__init__.py` are not packages. They cannot be imported. PYTHONPATH tricks do not make them proper packages; they just paper over the structural problem.

## Subagent Instructions

When spawning subagents for Python tasks, explicitly instruct them to use `uv run` in every command. Example:

> "Use `uv run` for every Python command in this project. Never invoke `python` or `python3` directly."

This prevents subagents from falling back to system Python or activating the wrong venv.

## pyproject.toml Template

```toml
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "numpy>=2.0",
]

[project.optional-dependencies]
dev = ["pytest", "ruff"]

[project.scripts]
my-command = "mypackage.module:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

Key points:
- `requires-python` pins the minimum Python version
- Entry points in `[project.scripts]`
- Dev deps in `[project.optional-dependencies]` or `[dependency-groups]`
- No `setup.py`, no `setup.cfg`, no `requirements.txt`

## Checkpoint File Handling

For ML projects with large binary files (`.npy`, `.pkl`, `.pt`, `.safetensors`):

- Do **not** track them in git (add to `.gitignore`)
- Use `uv` for Python tooling only; binary data lives outside the package manager
- For checkpoint loading/saving, prefer pickle (`.pkl`) over `np.savez` when working with MLX (avoids flatten/unflatten name mismatch issues)
- Document checkpoint format in code comments or architecture docs

Related: [[pokemon_tcg_mlx_migration]] (checkpoint format decisions).

## Quick Reference

| Task | Command |
|---|---|
| Run a script | `uv run script.py` |
| Run a project command | `uv run my-command` |
| Run a one-off tool | `uvx ruff check .` |
| Install a project dep | `uv add numpy` |
| Install a global CLI | `uv tool install kaggle` |
| Sync after toml change | `uv sync` |
| Run with env file | `uv run --env-file .env script.py` |
| Run tests | `uv run pytest` |
| Format code | `uv run ruff format .` |
| Lint code | `uv run ruff check .` |

## Related Pages

- [[preferencias-tecnicas]] — The directive that mandates uv as package manager
- [[pokemon_tcg_mlx_migration]] — Project where PYTHONPATH hacks were eliminated
- [[editorial-workflow]] — Wikifita editorial rules
- [[co-fita-infrastructure]] — Docker + uv patterns in Co-Fita stack
