| name | python-uv-setup |
| description | Sets up a modern Python project with uv, ruff, ty, and best practices for AI agent compatibility. Creates publishable packages with proper structure, type hints, and documentation. |
Python UV Project Setup
Activation
When this skill is triggered, ALWAYS display this banner first:
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ ๐ SKILL ACTIVATED: python-uv-setup โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Project: [project-name] โ
โ Action: Setting up modern Python project with uv... โ
โ Output: pyproject.toml, src layout, tests, CI โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
When to Use
This skill activates when:
- "set up a new Python project"
- "create a Python package"
- "initialize Python with uv"
- "modern Python setup"
- "migrate from conda/poetry/pip to uv"
- "convert requirements.txt to uv"
- Need a clean, agent-friendly Python project structure
Quick Start
Option 1: Use the bootstrap script (recommended)
bash .pi/skills/python-uv-setup/scripts/bootstrap.sh my-package
bash .pi/skills/python-uv-setup/scripts/bootstrap.sh my-cli --app
This creates a complete project with:
- src layout, tests, CI
- pyproject.toml with ruff, pytest, coverage config
- Type hints and py.typed marker
- Git initialized with first commit
Option 2: Manual setup
mkdir my-package && cd my-package
uv init --lib --name my-package
uv init --app --name my-app
Migrating Existing Projects
Brown-field (existing codebase) policy
When the repo already contains Python code, prioritize minimal change. The goal is uv compliance without altering application behavior.
Rules:
- Do not change application code unless explicitly requested.
- Do not move files unless explicitly allowed by the user.
- Preserve existing layout (do not force a
src/ layout on brown-field repos).
- Preserve declared Python support (do not auto-bump to 3.12 if the project advertises 3.10+) unless the user explicitly states a main version (e.g., โPython 3.12 is the mainโ). In that case, set
.python-version and requires-python to match the stated main version.
Brown-field steps (minimal, best practice):
- Read
README.md, requirements*.txt, setup.cfg/setup.py, pyproject.toml, and CI config to capture name, version, Python version range, and dependencies.
- If no
pyproject.toml, create one with [project] metadata (name, version, description, readme, license). Reuse existing values when available.
- Dependencies: import from existing files and pin exact versions (use
uv pip compile if needed). Preserve separation of runtime vs dev deps.
- Add
.python-version matching the projectโs supported Python (do not bump without permission).
- Add
.venv/ to .gitignore (keep existing ignore rules intact).
- Generate
uv.lock (via uv lock or uv sync) and commit it.
- Tooling configs (ruff/ty/pytest): may be added, but keep them conservative and do not enforce in CI unless the user requests.
- If CI exists, update it to use uv only if requested; otherwise leave CI untouched.
Quick Migration Script
bash .pi/skills/python-uv-setup/scripts/migrate.sh
bash .pi/skills/python-uv-setup/scripts/migrate.sh --from poetry
bash .pi/skills/python-uv-setup/scripts/migrate.sh --from conda
bash .pi/skills/python-uv-setup/scripts/migrate.sh --from requirements
The script will:
- Backup existing lock files and requirements
- Create/update pyproject.toml
- Import dependencies
- Run
uv sync
From requirements.txt
uv init --bare
uv add $(cat requirements.txt | grep -v '^#' | grep -v '^-' | tr '\n' ' ')
uv pip compile requirements.txt -o requirements.lock
uv add -r requirements.txt
uv add --dev -r requirements-dev.txt
uv sync
uv run python -c "import your_package"
Manual alternative - Add to pyproject.toml:
[project]
dependencies = [
"requests>=2.28.0",
"pandas>=2.0.0",
]
From Poetry (pyproject.toml + poetry.lock)
poetry export -f requirements.txt > requirements-poetry.txt
Before (Poetry):
[tool.poetry]
name = "my-package"
version = "0.1.0"
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.28"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
After (uv):
[project]
name = "my-package"
version = "0.1.0"
requires-python = "==3.12.*"
dependencies = [
"requests==2.32.3",
]
[project.optional-dependencies]
dev = [
"pytest==8.3.5",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
rm poetry.lock
uv sync --all-extras
From Conda (environment.yml)
conda list --export > conda-packages.txt
grep -A 1000 "pip:" environment.yml | grep "^ -" | sed 's/^ - //' > pip-deps.txt
uv init --bare
uv add requests pandas numpy
Hybrid approach (conda for CUDA + uv for Python):
conda create -n myenv python=3.12 cudatoolkit=12.1 -y
conda activate myenv
uv sync
From pip + venv (no pyproject.toml)
pip freeze > requirements-frozen.txt
uv init --bare
uv add requests pandas
uv add --dev pytest ruff
rm -rf venv .venv
uv sync --all-extras
From setup.py / setup.cfg
setup.cfg:
[metadata]
name = my-package
version = 0.1.0
[options]
packages = find:
install_requires =
requests>=2.28
pandas>=2.0
pyproject.toml:
[project]
name = "my-package"
version = "0.1.0"
requires-python = "==3.12.*"
dependencies = [
"requests==2.32.3",
"pandas==2.2.3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
rm setup.py setup.cfg MANIFEST.in
uv sync
Migration Checklist
Project Structure (src layout)
Note: Use this structure for new/green-field projects. For brown-field repos, keep the existing layout unless the user explicitly asks to reorganize.
my-package/
โโโ .github/
โ โโโ workflows/
โ โโโ ci.yml # GitHub Actions CI
โโโ src/
โ โโโ my_package/ # Package code (underscore, not hyphen)
โ โโโ __init__.py # Package exports
โ โโโ py.typed # PEP 561 marker for type hints
โ โโโ core.py # Core functionality
โ โโโ cli.py # CLI entry point (optional)
โโโ tests/
โ โโโ __init__.py
โ โโโ conftest.py # pytest fixtures
โ โโโ test_core.py
โโโ docs/
โ โโโ README.md # Detailed documentation
โโโ .gitignore
โโโ .python-version # Pin Python version for uv
โโโ pyproject.toml # Project configuration (single source of truth)
โโโ README.md # Project overview
โโโ LICENSE # MIT recommended
โโโ uv.lock # Lockfile (commit this!)
pyproject.toml (Complete Template)
[project]
name = "my-package"
version = "0.1.0"
description = "A short description of your package"
readme = "README.md"
license = { text = "MIT" }
requires-python = "==3.12.*"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
keywords = ["keyword1", "keyword2"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Typing :: Typed",
]
dependencies = [
]
[project.optional-dependencies]
dev = [
"pytest==8.3.5",
"pytest-cov==6.0.0",
"ruff==0.9.4",
]
[project.scripts]
my-cli = "my_package.cli:main"
[project.urls]
Homepage = "https://github.com/username/my-package"
Documentation = "https://github.com/username/my-package#readme"
Repository = "https://github.com/username/my-package"
Issues = "https://github.com/username/my-package/issues"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
[tool.ruff]
target-version = "py311"
line-length = 88
src = ["src", "tests"]
[tool.ruff.lint]
select = [
"E",
"W",
"F",
"I",
"B",
"C4",
"UP",
"ARG",
"SIM",
"TCH",
"PTH",
"ERA",
"RUF",
]
ignore = [
"E501",
]
[tool.ruff.lint.isort]
known-first-party = ["my_package"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
]
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
"raise NotImplementedError",
]
Essential Files
src/my_package/init.py
"""My Package - A short description."""
from my_package.core import main_function
__version__ = "0.1.0"
__all__ = ["main_function", "__version__"]
src/my_package/py.typed
# PEP 561 marker - this package supports type checking
(Empty file, just needs to exist)
src/my_package/core.py
"""Core functionality for my-package."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
def main_function(items: Sequence[str]) -> list[str]:
"""Process items and return results.
Args:
items: Input items to process.
Returns:
Processed items as a list.
Example:
>>> main_function(["a", "b", "c"])
['A', 'B', 'C']
"""
return [item.upper() for item in items]
tests/conftest.py
"""Pytest configuration and fixtures."""
import pytest
@pytest.fixture
def sample_data() -> list[str]:
"""Provide sample test data."""
return ["apple", "banana", "cherry"]
tests/test_core.py
"""Tests for core functionality."""
from my_package.core import main_function
def test_main_function(sample_data: list[str]) -> None:
"""Test main_function processes items correctly."""
result = main_function(sample_data)
assert result == ["APPLE", "BANANA", "CHERRY"]
def test_main_function_empty() -> None:
"""Test main_function handles empty input."""
assert main_function([]) == []
.python-version
3.12
New projects: pin to 3.12 for consistency.
Brown-field: match the project's existing supported Python version unless the user explicitly names a main version, in which case use that.
.gitignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
*.egg
dist/
build/
# Virtual environments
.venv/
venv/
ENV/
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# Type checking
.mypy_cache/
# IDEs
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Project specific
*.log
.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Set up Python 3.12
run: uv python install 3.12
- name: Install dependencies
run: uv sync --all-extras
- name: Lint with ruff
run: |
uv run ruff check .
uv run ruff format --check .
- name: Type check with ty
run: uv run ty check src/
- name: Test with pytest
run: uv run pytest --cov --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
Commands Reference
Daily Development
uv sync
uv sync --all-extras
uv run python -m my_package
uv run my-cli
uv run pytest
uv run pytest -v --cov
uv run ruff check .
uv run ruff check . --fix
uv run ruff format .
ty check src/
Dependency Management
uv add requests
uv add --dev pytest-xdist
uv remove requests
uv lock --upgrade
uv lock --upgrade-package requests
Building and Publishing
uv build
uv publish
uv publish --publish-url https://test.pypi.org/legacy/
Version Pinning (Strict)
Always pin exact versions - no >=, >, ^, or ~:
dependencies = [
"requests==2.32.3",
"pandas==2.2.3",
"numpy==2.2.2",
]
dependencies = [
"requests>=2.28",
"pandas^2.0",
"numpy~=2.0",
]
Why exact versions:
- Reproducible builds across all environments
- No surprise breakages from upstream updates
- Easier debugging (everyone has same versions)
- AI agents can rely on specific API behavior
To get current versions:
uv pip show requests | grep Version
uv add requests
cat uv.lock | grep -A2 '"requests"'
Python version:
# .python-version - new projects: 3.12; brown-field: match existing
3.12
requires-python = "==3.12.*"
Agent-Friendly Best Practices
1. Type Everything
def process(data: dict[str, int]) -> list[tuple[str, int]]:
return [(k, v) for k, v in data.items()]
def process(data):
return [(k, v) for k, v in data.items()]
2. Docstrings with Examples
def calculate_discount(price: float, percent: float) -> float:
"""Calculate discounted price.
Args:
price: Original price in dollars.
percent: Discount percentage (0-100).
Returns:
Discounted price.
Raises:
ValueError: If percent is not between 0 and 100.
Example:
>>> calculate_discount(100.0, 20.0)
80.0
"""
3. Explicit Exports
__all__ = [
"MainClass",
"helper_function",
"CONSTANT",
]
4. Structured Errors
class MyPackageError(Exception):
"""Base exception for my-package."""
class ValidationError(MyPackageError):
"""Raised when validation fails."""
def __init__(self, field: str, message: str) -> None:
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
5. Configuration via Pydantic (optional)
uv add pydantic pydantic-settings
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
api_key: str
debug: bool = False
max_retries: int = 3
model_config = {"env_prefix": "MY_PACKAGE_"}
Checklist