# uv Project Layout and Dependency Management !!! info "Primary sources" - [uv project guide](https://docs.astral.sh/uv/guides/projects/) - [uv project layout](https://docs.astral.sh/uv/concepts/projects/layout/) - [uv Docker integration](https://docs.astral.sh/uv/guides/integration/docker/) --- ## Core Files | File | Purpose | Commit? | |------|---------|---------| | `pyproject.toml` | Project metadata, deps, tool config | Yes | | `uv.lock` | Exact resolved versions, cross-platform | **Yes** | | `.python-version` | Default Python version for the project | Yes | | `.venv/` | Local virtual environment | No (`.gitignore`) | !!! warning "Commit the lockfile" `uv.lock` must be committed. It is the source of truth for reproducible installs in CI and Docker. Never edit it by hand. --- ## Essential Commands ```bash # Initialize a new project (app, not library) uv init --app # Initialize a library (installable package with src layout) uv init --lib # Add a runtime dependency uv add fastapi[standard] # Add multiple dependencies at once uv add uvicorn[standard] pydantic-settings # Add dev-only dependency uv add --dev pytest httpx pytest-asyncio ruff mypy # Remove a dependency uv remove requests # Upgrade a specific package (keeps rest of lockfile intact) uv lock --upgrade-package fastapi # Upgrade all packages uv lock --upgrade # Sync env to lockfile (install/remove as needed) uv sync # Sync without dev deps (e.g., in CI or Docker) uv sync --no-dev # Sync and assert lockfile is up-to-date (for CI / Docker) uv sync --locked # Run a command in the project environment uv run pytest uv run uvicorn my_app.main:app --reload # Run a one-off command without installing anything permanently uv run --with httpx python -c "import httpx; print(httpx.__version__)" ``` --- ## pyproject.toml Reference ```toml [project] name = "my-app" version = "0.1.0" description = "Production FastAPI service" requires-python = ">=3.12" license = { text = "MIT" } readme = "README.md" dependencies = [ "fastapi[standard]>=0.115", "uvicorn[standard]>=0.34", "pydantic-settings>=2.0", ] [project.scripts] # Creates `my-app` CLI entry point when installed my-app = "my_app.main:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/my_app"] [tool.uv] dev-dependencies = [ "pytest>=8", "pytest-asyncio>=0.24", "httpx>=0.27", # needed for FastAPI TestClient "ruff>=0.6", "mypy>=1.11", ] [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" strict-markers = true [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "S"] [tool.mypy] strict = true ``` --- ## src Layout with uv Use `uv init --lib` or set up manually: ``` my-app/ ├── pyproject.toml ├── uv.lock ├── .python-version # e.g., "3.12" ├── .env.example ├── README.md ├── src/ │ └── my_app/ │ └── __init__.py └── tests/ └── conftest.py ``` The `src/` layout prevents the local directory from shadowing the installed package, which would otherwise cause silent test failures when testing the installed version. **hatchling config for src layout:** ```toml [tool.hatch.build.targets.wheel] packages = ["src/my_app"] ``` --- ## Migration from pip / requirements.txt ```bash # 1. Initialize uv in an existing project uv init --no-workspace # if already has pyproject.toml, skip this # 2. Import from requirements.txt uv add -r requirements.txt # 3. Import dev requirements uv add --dev -r requirements-dev.txt # 4. Verify lockfile was created cat uv.lock | head -20 # 5. Clean up old files rm requirements.txt requirements-dev.txt setup.py setup.cfg Pipfile Pipfile.lock # 6. Add .venv to .gitignore echo ".venv/" >> .gitignore ``` --- ## uv in Docker The canonical pattern uses `--mount=type=cache` for fast rebuilds and `--no-install-project` for layer separation: ```dockerfile # Copy uv binary (pin the version) COPY --from=ghcr.io/astral-sh/uv:0.5.27 /uv /uvx /bin/ WORKDIR /app # Layer 1: install dependencies (changes rarely → cached aggressively) RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-editable # Layer 2: copy source and install project (changes frequently) COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-editable ``` **Key flags:** | Flag | Effect | |------|--------| | `--locked` | Fail if `uv.lock` is out of date with `pyproject.toml` | | `--no-install-project` | Install deps but not the project itself (layer separation) | | `--no-editable` | Install in non-editable mode (copy code into `.venv`, not symlink) | | `--no-dev` | Skip dev dependencies (use in production images) | | `--compile-bytecode` | Pre-compile `.pyc` files (faster startup, larger image) | **After syncing, activate the venv via PATH (not `uv run`) in production:** ```dockerfile ENV PATH="/app/.venv/bin:$PATH" CMD ["uvicorn", "my_app.main:app", "--host", "0.0.0.0", "--port", "8000"] ``` Using `CMD ["uv", "run", "uvicorn", ...]` in production is fine but adds a small overhead and requires uv to be present in the final image. --- ## CI/CD with uv ```yaml # GitHub Actions example - name: Install uv uses: astral-sh/setup-uv@v4 with: version: "0.5.27" # pin for reproducibility - name: Sync dependencies run: uv sync --locked - name: Run tests run: uv run pytest --tb=short - name: Lint run: uv run ruff check . - name: Type check run: uv run mypy src/ ``` --- ## .gitignore Entries for uv Projects ``` .venv/ __pycache__/ *.pyc .env dist/ *.egg-info/ .mypy_cache/ .ruff_cache/ .pytest_cache/ ``` **Do NOT ignore `uv.lock`** — it must be committed.