
Python Uv
- 81 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with python tasks during AI-assisted development.
About
python-uv is a Claude Code skill for python. It helps solo builders move faster with AI-assisted coding.
- python-uv
- Python
- AI-coding skill
Python Uv by the numbers
- 81 all-time installs (skills.sh)
- +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #117 of 290 Python skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill python-uvAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 81 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with python tasks during AI-assisted development.
Files
Python uv
Overview
uv is an extremely fast Python package and project manager written in Rust, designed to replace pip, pip-tools, pipx, poetry, pyenv, and virtualenv. It provides unified project management, dependency resolution, Python version management, and tool execution with 10-100x speed improvements over traditional tools.
Key capabilities: Project initialization, dependency locking and syncing, Python version management, PEP 723 inline script dependencies, ephemeral tool execution via uvx, monorepo workspaces with shared lockfiles, and package building/publishing.
When to use: Python project initialization, dependency management, virtual environments, Python version pinning, running scripts with inline dependencies, monorepo workspaces, tool execution, publishing packages.
When NOT to use: Non-Python projects, conda-managed scientific computing environments with system-level binary dependencies, projects locked to legacy setup.py-only workflows.
Quick Reference
| Pattern | Command / API | Key Points |
|---|---|---|
| Init project | uv init | Creates pyproject.toml and .python-version |
| Init library | uv init --lib | Creates src/ layout with py.typed |
| Init script | uv init --script example.py | PEP 723 inline metadata script |
| Add dependency | uv add requests | Adds to pyproject.toml, updates lockfile |
| Add dev dependency | uv add --dev pytest | Adds to [dependency-groups] dev group |
| Add group dependency | uv add --group docs mkdocs | Custom dependency groups |
| Add optional | uv add --optional postgres psycopg | Optional extras for libraries |
| Remove dependency | uv remove requests | Removes from pyproject.toml and lockfile |
| Lock dependencies | uv lock | Creates/updates uv.lock |
| Upgrade in lockfile | uv lock --upgrade-package requests | Targeted dependency upgrade |
| Sync environment | uv sync | Installs locked dependencies into .venv |
| Sync for CI | uv sync --locked | Fails if lockfile is stale |
| Sync frozen | uv sync --frozen | Skips lockfile verification |
| Run command | uv run python app.py | Runs in project virtual environment |
| Run script | uv run script.py | Supports PEP 723 inline dependencies |
| Run in package | uv run --package api pytest | Workspace-specific execution |
| Install Python | uv python install 3.13 | Downloads and manages Python versions |
| Pin Python | uv python pin 3.12 | Writes .python-version file |
| List Pythons | uv python list | Shows available and installed versions |
| Run tool | uvx ruff check . | Ephemeral tool execution |
| Tool with plugins | uvx --with mkdocs-material mkdocs | Ephemeral tool with extra packages |
| Install tool | uv tool install ruff | Persistent global tool install |
| Workspace | [tool.uv.workspace] | Monorepo multi-package support |
| Build package | uv build | Creates sdist and wheel in dist/ |
| Publish | uv publish | Uploads to PyPI with trusted publishing |
| Export deps | uv export --format requirements-txt | Generate requirements.txt from lockfile |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using pip install inside uv project | uv add to manage via pyproject.toml |
Activating venv manually before uv run | uv run handles venv activation automatically |
Committing .venv/ to version control | Add .venv/ to .gitignore, commit uv.lock |
Not committing uv.lock | Always commit uv.lock for reproducible builds |
Using uv sync without --locked in CI | uv sync --locked ensures lockfile matches pyproject.toml |
Running uv lock --upgrade routinely | Only upgrade intentionally, use --upgrade-package for targeted updates |
Mixing pip and uv dependency management | Choose one tool for the project consistently |
Using uv pip install for project deps | Use uv add/uv sync for managed projects |
Forgetting --frozen for Docker builds | uv sync --frozen skips lockfile verification for faster builds |
| Creating venv manually in uv project | uv sync creates and manages .venv automatically |
Using setup.py for new projects | Use pyproject.toml with a modern build backend |
Not using py.typed in libraries | uv init --lib includes it, required for typed packages |
Delegation
- Project scaffolding: Use
Exploreagent - Dependency audit: Use
Taskagent - Code review: Delegate to
code-revieweragent
If the docker skill is available, delegate containerization patterns to it.If the github-actions skill is available, delegate CI/CD pipeline configuration to it.If the api-testing skill is available, delegate API testing patterns to it.If the sentry-setup-logging skill is available, delegate error monitoring setup to it.If the pino-logging skill is available, delegate Node.js logging patterns to it (Python equivalent covered here with structlog).References
- Project management and pyproject.toml configuration
- Dependency management, lockfiles, and groups
- Python version management and virtual environments
- Scripts, inline dependencies, and tool management
- Workspace support for monorepos
- FastAPI web framework patterns
- Pydantic validation and data modeling
- Async patterns with asyncio
- Type checking with mypy and pyright
- Testing with pytest
- Logging with structlog
- CLI applications with typer
- Docker integration and publishing
Async Patterns
Basic Async Function
import asyncio
import httpx
async def fetch_url(url: str) -> str:
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.textTask Groups (Structured Concurrency)
async def fetch_all(urls: list[str]) -> list[str]:
results: list[str] = []
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch_url(url)) for url in urls]
return [task.result() for task in tasks]TaskGroup raises ExceptionGroup if any task fails. All tasks are cancelled on first failure.
Concurrency Limiting with Semaphore
async def fetch_with_limit(urls: list[str], max_concurrent: int = 10) -> list[str]:
semaphore = asyncio.Semaphore(max_concurrent)
async def limited_fetch(url: str) -> str:
async with semaphore:
return await fetch_url(url)
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(limited_fetch(url)) for url in urls]
return [task.result() for task in tasks]Async Context Managers
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
@asynccontextmanager
async def db_connection(url: str) -> AsyncIterator[Connection]:
conn = await connect(url)
try:
yield conn
finally:
await conn.close()
async def query_users() -> list[User]:
async with db_connection("postgresql://localhost/mydb") as conn:
return await conn.fetch("SELECT * FROM users")Async Iterators
from collections.abc import AsyncIterator
async def stream_lines(path: str) -> AsyncIterator[str]:
import aiofiles
async with aiofiles.open(path) as f:
async for line in f:
yield line.strip()
async def process_file(path: str) -> None:
async for line in stream_lines(path):
await process_line(line)Timeouts
async def fetch_with_timeout(url: str, timeout: float = 5.0) -> str:
async with asyncio.timeout(timeout):
return await fetch_url(url)Queue-Based Producer/Consumer
async def producer(queue: asyncio.Queue[str], items: list[str]) -> None:
for item in items:
await queue.put(item)
async def consumer(queue: asyncio.Queue[str], name: str) -> None:
while True:
item = await queue.get()
await process(item)
queue.task_done()
async def run_pipeline(items: list[str], num_workers: int = 3) -> None:
queue: asyncio.Queue[str] = asyncio.Queue(maxsize=100)
async with asyncio.TaskGroup() as tg:
tg.create_task(producer(queue, items))
for i in range(num_workers):
tg.create_task(consumer(queue, f"worker-{i}"))
await queue.join()Event Coordination
async def waiter(event: asyncio.Event) -> None:
await event.wait()
print("Event received")
async def setter(event: asyncio.Event) -> None:
await asyncio.sleep(1)
event.set()
async def coordinate() -> None:
event = asyncio.Event()
async with asyncio.TaskGroup() as tg:
tg.create_task(waiter(event))
tg.create_task(setter(event))Retry Pattern
import asyncio
import random
async def retry_async[T](
func,
*args,
max_retries: int = 3,
base_delay: float = 1.0,
) -> T:
for attempt in range(max_retries):
try:
return await func(*args)
except Exception:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
await asyncio.sleep(delay)
raise RuntimeError("Unreachable")Running Async Code
async def main() -> None:
result = await fetch_url("https://example.com")
print(result)
if __name__ == "__main__":
asyncio.run(main())CLI Applications
Setup
uv add typer
uv add --dev pytestBasic CLI
import typer
app = typer.Typer()
@app.command()
def hello(name: str) -> None:
print(f"Hello {name}")
if __name__ == "__main__":
app()uv run python cli.py AliceCommands and Subcommands
import typer
app = typer.Typer(help="User management CLI")
users_app = typer.Typer(help="User operations")
app.add_typer(users_app, name="users")
@users_app.command("list")
def list_users(
limit: int = typer.Option(10, help="Max users to display"),
active: bool = typer.Option(True, help="Only show active users"),
) -> None:
typer.echo(f"Listing {limit} users (active={active})")
@users_app.command("create")
def create_user(
name: str = typer.Argument(help="User's full name"),
email: str = typer.Option(..., help="User's email"),
admin: bool = typer.Option(False, "--admin", help="Grant admin role"),
) -> None:
typer.echo(f"Created user: {name} ({email}) admin={admin}")
@app.command()
def version() -> None:
typer.echo("v1.0.0")uv run python cli.py users list --limit 5
uv run python cli.py users create "Alice" --email alice@example.com --admin
uv run python cli.py versionArguments and Options
from enum import Enum
from pathlib import Path
from typing import Annotated, Optional
import typer
class OutputFormat(str, Enum):
json = "json"
table = "table"
csv = "csv"
@app.command()
def export(
path: Annotated[Path, typer.Argument(help="Output file path")],
format: Annotated[OutputFormat, typer.Option(help="Output format")] = OutputFormat.json,
verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False,
tags: Annotated[Optional[list[str]], typer.Option("--tag", "-t")] = None,
) -> None:
if verbose:
typer.echo(f"Exporting to {path} as {format.value}")
if tags:
typer.echo(f"Tags: {', '.join(tags)}")Rich Output
import typer
from rich.console import Console
from rich.table import Table
console = Console()
@app.command()
def status() -> None:
table = Table(title="Service Status")
table.add_column("Service", style="cyan")
table.add_column("Status", style="green")
table.add_column("Uptime")
table.add_row("API", "Running", "3d 14h")
table.add_row("Worker", "Running", "3d 14h")
table.add_row("Database", "Running", "30d 2h")
console.print(table)Progress Bars
import time
import typer
@app.command()
def process(items: int = 100) -> None:
with typer.progressbar(range(items), label="Processing") as progress:
for _item in progress:
time.sleep(0.01)
typer.echo("Done!")Error Handling
@app.command()
def deploy(environment: str) -> None:
if environment not in ("staging", "production"):
typer.echo(f"Unknown environment: {environment}", err=True)
raise typer.Exit(code=1)
if environment == "production":
confirmed = typer.confirm("Deploy to production?")
if not confirmed:
raise typer.Abort()
typer.echo(f"Deploying to {environment}")Entry Point Configuration
[project.scripts]
my-cli = "my_app.cli:app"uv run my-cli users list
uv run my-cli --helpTesting CLI
from typer.testing import CliRunner
from my_app.cli import app
runner = CliRunner()
def test_hello():
result = runner.invoke(app, ["hello", "Alice"])
assert result.exit_code == 0
assert "Hello Alice" in result.output
def test_version():
result = runner.invoke(app, ["version"])
assert result.exit_code == 0
assert "v1.0.0" in result.outputClick Alternative
import click
@click.group()
def cli() -> None:
pass
@cli.command()
@click.argument("name")
@click.option("--count", default=1, help="Number of greetings")
def hello(name: str, count: int) -> None:
for _ in range(count):
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
cli()Dependency Management
Adding Dependencies
uv add requests
uv add "requests>=2.31,<3"
uv add fastapi uvicornDev Dependencies
uv add --dev pytest ruff mypyAdds to [dependency-groups]:
[dependency-groups]
dev = [
"pytest>=8.0",
"ruff>=0.8.0",
"mypy>=1.13",
]Custom Groups
uv add --group docs mkdocs mkdocs-material
uv add --group test pytest pytest-cov pytest-asyncio[dependency-groups]
docs = [
"mkdocs>=1.6",
"mkdocs-material>=9.5",
]
test = [
"pytest>=8.0",
"pytest-cov>=6.0",
"pytest-asyncio>=0.24",
]Optional Dependencies
uv add --optional postgres "psycopg[binary]>=3.0"[project.optional-dependencies]
postgres = ["psycopg[binary]>=3.0"]Removing Dependencies
uv remove requests
uv remove --dev ruff
uv remove --group docs mkdocsVersion Constraints
uv add "requests>=2.31"
uv add "requests>=2.31,<3"
uv add "requests~=2.31"
uv add "requests==2.31.0"Sources
Configure alternative package sources in pyproject.toml:
Git Sources
[tool.uv.sources]
my-lib = { git = "https://github.com/org/my-lib", tag = "v1.0.0" }Local Path Sources
[tool.uv.sources]
my-lib = { path = "../my-lib", editable = true }Workspace Sources
[tool.uv.sources]
shared = { workspace = true }Index Sources
[tool.uv.sources]
torch = { index = "pytorch" }
[[tool.uv.index]]
name = "pytorch"
url = "https://download.pytorch.org/whl/cpu"Lockfile Management
Create or Update Lockfile
uv lockUpgrade All Dependencies
uv lock --upgradeUpgrade Specific Package
uv lock --upgrade-package requestsVerify Lockfile Currency
uv lock --checkSyncing Environment
Install All Dependencies
uv syncSkip Dev Dependencies
uv sync --no-devSync Specific Groups
uv sync --group docs
uv sync --all-groupsInstall with Optional Dependencies
uv sync --extra postgres
uv sync --all-extrasCI/CD Sync Patterns
uv sync --locked
uv sync --frozenForce Reinstall
uv sync --reinstall
uv sync --reinstall-package requestsPrivate Package Indexes
[[tool.uv.index]]
name = "private"
url = "https://pypi.company.com/simple/"
[[tool.uv.index]]
name = "pytorch"
url = "https://download.pytorch.org/whl/cpu"
explicit = trueEnvironment Markers
[project]
dependencies = [
"uvloop>=0.21; sys_platform != 'win32'",
"winloop>=0.1; sys_platform == 'win32'",
]Exporting Requirements
uv export --format requirements-txt > requirements.txt
uv export --format requirements-txt --no-dev > requirements.txtDocker and Publishing
Dockerfile with uv
FROM python:3.12-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
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-dev
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
ENV PATH="/app/.venv/bin:$PATH"
CMD ["python", "-m", "my_app"]Multi-Stage Build
FROM python:3.12-slim-bookworm AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app
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-dev
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
FROM python:3.12-slim-bookworm
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/src /app/src
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["uvicorn", "my_app.main:app", "--host", "0.0.0.0", "--port", "8000"]Pinning uv Version in Docker
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /uvx /bin/Docker Compose
services:
api:
build: .
ports:
- '8000:8000'
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/mydb
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Development Dockerfile
FROM python:3.12-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
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
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked
ENV PATH="/app/.venv/bin:$PATH"
CMD ["uvicorn", "my_app.main:app", "--reload", "--host", "0.0.0.0"]Building Packages
uv buildCreates dist/ with:
my_package-0.1.0.tar.gz(sdist)my_package-0.1.0-py3-none-any.whl(wheel)
Build Specific Workspace Package
uv build --package my-libPublishing to PyPI
Configure Trusted Publishing (GitHub Actions)
name: Publish
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
environment:
name: pypi
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
- run: uv python install 3.13
- run: uv build
- run: uv publishPublish with Token
uv publish --token $PYPI_TOKENPublish to TestPyPI
uv publish --publish-url https://test.pypi.org/legacy/ --token $TEST_PYPI_TOKENGitHub Actions CI
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
- run: uv python install ${{ matrix.python-version }}
- run: uv sync --locked
- run: uv run pytest --cov
- run: uv run mypy src/
- run: uv run ruff check ..dockerignore
.venv/
__pycache__/
*.pyc
.git/
.github/
dist/
*.egg-info/
.mypy_cache/
.pytest_cache/
.ruff_cache/FastAPI Patterns
Project Setup
uv init my-api
cd my-api
uv add fastapi "uvicorn[standard]"
uv add --dev pytest httpx pytest-asyncioApplication Structure
src/
my_api/
__init__.py
main.py
config.py
dependencies.py
routers/
__init__.py
users.py
items.py
models/
__init__.py
user.py
schemas/
__init__.py
user.pyApplication Entry Point
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from my_api.routers import items, users
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
# Startup: initialize resources
yield
# Shutdown: cleanup resources
app = FastAPI(title="My API", lifespan=lifespan)
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(items.router, prefix="/items", tags=["items"])Router Pattern
from fastapi import APIRouter, Depends, HTTPException, status
from my_api.dependencies import get_db
from my_api.schemas.user import UserCreate, UserResponse
router = APIRouter()
@router.get("/", response_model=list[UserResponse])
async def list_users(db: Database = Depends(get_db)):
return await db.fetch_all("SELECT * FROM users")
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: Database = Depends(get_db)):
user = await db.fetch_one("SELECT * FROM users WHERE id = :id", {"id": user_id})
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate, db: Database = Depends(get_db)):
user_id = await db.execute("INSERT INTO users (name, email) VALUES (:name, :email)", payload.model_dump())
return {**payload.model_dump(), "id": user_id}Dependency Injection
from collections.abc import AsyncIterator
from typing import Annotated
from fastapi import Depends, Header, HTTPException, status
async def get_db() -> AsyncIterator[Database]:
db = Database()
try:
yield db
finally:
await db.disconnect()
async def get_current_user(
authorization: Annotated[str, Header()],
db: Database = Depends(get_db),
) -> User:
token = authorization.removeprefix("Bearer ")
user = await db.fetch_one("SELECT * FROM users WHERE token = :token", {"token": token})
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
@router.get("/me")
async def get_me(user: CurrentUser) -> UserResponse:
return userMiddleware
import time
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Process-Time"] = str(duration)
return response
app.add_middleware(TimingMiddleware)Error Handling
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class AppError(Exception):
def __init__(self, message: str, status_code: int = 400):
self.message = message
self.status_code = status_code
@app.exception_handler(AppError)
async def app_error_handler(_request: Request, exc: AppError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.message},
)Running the Server
uv run uvicorn my_api.main:app --reload --host 0.0.0.0 --port 8000[project.scripts]
serve = "uvicorn my_api.main:app --reload"Project Management
Initialize a Project
uv init my-project
cd my-projectCreates:
my-project/
pyproject.toml
.python-version
hello.pyApplication vs Library
uv init my-app
uv init --lib my-libLibrary layout uses src/ structure with py.typed marker:
my-lib/
pyproject.toml
src/
my_lib/
__init__.py
py.typedInit with Specific Python Version
uv init --python 3.12 my-projectpyproject.toml Configuration
Application
[project]
name = "my-app"
version = "0.1.0"
description = "My application"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn>=0.34.0",
]
[dependency-groups]
dev = [
"pytest>=8.0",
"ruff>=0.8.0",
"mypy>=1.13",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Library
[project]
name = "my-lib"
version = "0.1.0"
description = "My library"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.0",
]
readme = "README.md"
license = { text = "MIT" }
authors = [
{ name = "Author", email = "author@example.com" },
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
[project.urls]
Homepage = "https://github.com/org/my-lib"
Documentation = "https://my-lib.readthedocs.io"
[project.optional-dependencies]
postgres = ["psycopg[binary]>=3.0"]
[dependency-groups]
dev = [
"pytest>=8.0",
"ruff>=0.8.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Build System Options
Hatchling (default)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"uv Build Backend
[build-system]
requires = ["uv_build>=0.10.4,<0.11.0"]
build-backend = "uv_build"Setuptools
[build-system]
requires = ["setuptools>=75.0"]
build-backend = "setuptools.build_meta"Entry Points
Console Scripts
[project.scripts]
my-cli = "my_app.cli:main"uv run my-cliGUI Scripts
[project.gui-scripts]
my-gui = "my_app.gui:main"Tool Configuration in pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
[tool.mypy]
strict = true
python_version = "3.12"
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"uv-Specific Configuration
[tool.uv]
dev-dependencies = ["pytest>=8.0"]
[tool.uv.sources]
my-lib = { path = "../my-lib" }Project Commands
uv run python -m my_app
uv run pytest
uv run ruff check .
uv run mypy src/Pydantic Validation
Setup
uv add "pydantic>=2.0"
uv add pydantic-settingsBasic Models
from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: EmailStr
age: int = Field(ge=0, le=150)
class UserResponse(BaseModel):
id: int
name: str
email: str
model_config = {"from_attributes": True}Validation
from pydantic import BaseModel, field_validator, model_validator
class OrderCreate(BaseModel):
items: list[str]
quantity: int = Field(gt=0)
discount_code: str | None = None
@field_validator("items")
@classmethod
def items_not_empty(cls, v: list[str]) -> list[str]:
if not v:
raise ValueError("Order must have at least one item")
return v
@model_validator(mode="after")
def validate_discount(self) -> "OrderCreate":
if self.discount_code and self.quantity < 5:
raise ValueError("Discount requires minimum 5 items")
return selfSerialization
user = UserResponse(id=1, name="Alice", email="alice@example.com")
user.model_dump()
user.model_dump(exclude={"email"})
user.model_dump(exclude_none=True)
user.model_dump_json()
UserResponse.model_validate({"id": 1, "name": "Alice", "email": "alice@example.com"})
UserResponse.model_validate_json('{"id": 1, "name": "Alice", "email": "alice@example.com"}')Nested Models
from datetime import datetime
class Address(BaseModel):
street: str
city: str
country: str = "US"
class UserProfile(BaseModel):
user: UserResponse
address: Address
created_at: datetime
tags: list[str] = []Generic Models
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
per_page: int
@property
def has_next(self) -> bool:
return self.page * self.per_page < self.totalDiscriminated Unions
from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field
class EmailNotification(BaseModel):
type: Literal["email"] = "email"
to: str
subject: str
class SMSNotification(BaseModel):
type: Literal["sms"] = "sms"
phone: str
message: str
Notification = Annotated[
Union[EmailNotification, SMSNotification],
Field(discriminator="type"),
]
class Event(BaseModel):
notification: NotificationSettings Management
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="APP_",
)
debug: bool = False
database_url: str
redis_url: str = "redis://localhost:6379"
secret_key: str = Field(min_length=32)
allowed_origins: list[str] = ["http://localhost:3000"]APP_DATABASE_URL=postgresql://localhost/mydb
APP_SECRET_KEY=supersecretkeythatis32charslong!!settings = Settings()Custom Types
from typing import Annotated
from pydantic import AfterValidator, BaseModel
def validate_slug(v: str) -> str:
if not v.replace("-", "").isalnum():
raise ValueError("Slug must be alphanumeric with hyphens")
return v.lower()
Slug = Annotated[str, AfterValidator(validate_slug)]
class Article(BaseModel):
title: str
slug: SlugModel Config Options
from pydantic import BaseModel, ConfigDict
class StrictModel(BaseModel):
model_config = ConfigDict(
strict=True,
frozen=True,
from_attributes=True,
populate_by_name=True,
str_strip_whitespace=True,
)Python Environments
Python Version Management
Install Python Versions
uv python install 3.13
uv python install 3.11 3.12 3.13
uv python install pypy@3.10List Available Versions
uv python list
uv python list --only-installedPin Project Python Version
uv python pin 3.12Creates .python-version:
3.12Upgrade to Latest Patch
uv python upgrade 3.12
uv python upgradeFind Python Interpreter
uv python find 3.12Virtual Environments
Automatic Environment Management
uv creates and manages .venv automatically:
uv syncThis creates .venv/ in the project root and installs all locked dependencies.
Manual Virtual Environment Creation
uv venv
uv venv --python 3.12
uv venv .venv-test --python 3.11Using Specific Environment
UV_PROJECT_ENVIRONMENT=/path/to/venv uv syncEnvironment Behavior
How uv run Works
uv run python app.pyThis:
1. Finds the project pyproject.toml 2. Creates .venv if missing 3. Syncs dependencies if needed 4. Runs the command in the virtual environment
No Manual Activation Needed
uv run pytest
uv run python -c "import sys; print(sys.version)"
uv run uvicorn app:app --reloadRunning with Extra Dependencies
uv run --extra postgres python -c "import psycopg"requires-python Configuration
Minimum Version
[project]
requires-python = ">=3.12"Bounded Range
[project]
requires-python = ">=3.11,<3.14"Multi-Version Testing
uv python install 3.11 3.12 3.13
uv run --python 3.11 pytest
uv run --python 3.12 pytest
uv run --python 3.13 pytest.python-version vs requires-python
.python-versionsets the default Python for the project directoryrequires-pythoninpyproject.tomldefines compatible Python rangeuv python pinwrites.python-versionuv syncrespectsrequires-pythonfor dependency resolution
gitignore Configuration
.venv/
__pycache__/
*.pyc
dist/
*.egg-info/Scripts and Tools
Running Scripts
Basic Script Execution
uv run python app.py
uv run python -m my_moduleScripts with Inline Dependencies (PEP 723)
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests<3",
# "rich",
# ]
# ///
import requests
from rich.pretty import pprint
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])uv run script.pyuv resolves and installs inline dependencies automatically in an isolated environment.
Initialize a Script
uv init --script example.py --python 3.12Add Dependencies to a Script
uv add --script example.py "requests<3" "rich"Lock Script Dependencies
uv lock --script example.pyShebang for Direct Execution
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx"]
# ///
import httpx
resp = httpx.get("https://example.com")
print(resp.status_code)chmod +x script.py
./script.pyTool Management with uvx
Ephemeral Tool Execution
uvx ruff check .
uvx black --check .
uvx mypy src/
uvx pycowsay "hello world"Tools run in temporary environments and are cleaned up automatically.
Specific Version
uvx ruff@0.8.0 check
uvx ruff@latest checkDifferent Package and Command Names
uvx --from httpie http example.com
uvx --from jupyter-core jupyterTools with Extras
uvx --from "mypy[faster-cache,reports]" mypy --xml-report report/Tools with Plugins
uvx --with mkdocs-material mkdocs build
uvx --with pytest-cov pytest --cov=src tests/Global Tool Installation
Install Tools
uv tool install ruff
uv tool install "httpie>0.1.0"
uv tool install git+https://github.com/astral-sh/ruffInstall with Additional Packages
uv tool install mkdocs --with mkdocs-material --with mkdocs-mermaid2-pluginUpgrade Tools
uv tool upgrade ruff
uv tool upgrade --allList Installed Tools
uv tool listUninstall Tools
uv tool uninstall ruffTool Install Directory
uv tool dirCommon Tool Recipes
Code Formatting
uvx ruff format .
uvx black .
uvx isort .Linting
uvx ruff check . --fix
uvx flake8 src/
uvx pylint src/Type Checking
uvx mypy src/
uvx pyright src/Security Scanning
uvx bandit -r src/
uvx pip-auditDocumentation
uvx --with mkdocs-material mkdocs serve
uvx sphinx-build docs/ docs/_build/Structlog Logging
Setup
uv add structlogBasic Configuration
import structlog
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
structlog.dev.set_exc_info,
structlog.processors.TimeStamper(fmt="iso"),
structlog.dev.ConsoleRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(0),
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)Production Configuration (JSON Output)
import structlog
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
structlog.dev.set_exc_info,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.dict_tracing.DictTracer(),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(0),
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)Basic Usage
import structlog
log = structlog.get_logger()
async def process_order(order_id: str, user_id: str) -> None:
log.info("processing_order", order_id=order_id, user_id=user_id)
try:
result = await charge_payment(order_id)
log.info("payment_charged", order_id=order_id, amount=result.amount)
except PaymentError:
log.error("payment_failed", order_id=order_id, exc_info=True)
raiseContext Binding
Logger-Level Binding
log = structlog.get_logger().bind(service="order-api", environment="production")
log.info("server_started", port=8000)Request-Scoped Context (contextvars)
import structlog
from structlog.contextvars import bind_contextvars, clear_contextvars
async def request_middleware(request: Request, call_next):
clear_contextvars()
bind_contextvars(
request_id=request.headers.get("x-request-id", "unknown"),
method=request.method,
path=request.url.path,
)
log.info("request_started")
response = await call_next(request)
log.info("request_completed", status_code=response.status_code)
return responseAll log entries within the request automatically include request_id, method, and path.
Custom Processors
def add_app_context(
logger: structlog.types.WrappedLogger,
method_name: str,
event_dict: structlog.types.EventDict,
) -> structlog.types.EventDict:
event_dict["app"] = "my-api"
event_dict["version"] = "1.0.0"
return event_dict
structlog.configure(
processors=[
add_app_context,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
)Filtering Sensitive Data
SENSITIVE_KEYS = {"password", "token", "secret", "authorization"}
def filter_sensitive(
logger: structlog.types.WrappedLogger,
method_name: str,
event_dict: structlog.types.EventDict,
) -> structlog.types.EventDict:
for key in SENSITIVE_KEYS:
if key in event_dict:
event_dict[key] = "***REDACTED***"
return event_dictFastAPI Integration
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
import structlog
from fastapi import FastAPI, Request
from structlog.contextvars import bind_contextvars, clear_contextvars
log = structlog.get_logger()
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
)
log.info("app_started")
yield
log.info("app_stopped")
app = FastAPI(lifespan=lifespan)
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
clear_contextvars()
bind_contextvars(request_id=request.headers.get("x-request-id", ""))
response = await call_next(request)
return responseException Logging
try:
await process_payment(order_id)
except Exception:
log.exception("payment_processing_failed", order_id=order_id)
raiseTesting Patterns
Setup
uv add --dev pytest pytest-cov pytest-asyncio httpx[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-ra -q"Project Structure
tests/
__init__.py
conftest.py
test_users.py
test_items.pyBasic Tests
def test_addition():
assert 1 + 1 == 2
def test_user_creation():
user = User(name="Alice", email="alice@example.com")
assert user.name == "Alice"
assert user.email == "alice@example.com"Fixtures
import pytest
@pytest.fixture
def sample_user() -> User:
return User(name="Alice", email="alice@example.com")
@pytest.fixture
def user_list() -> list[User]:
return [
User(name="Alice", email="alice@example.com"),
User(name="Bob", email="bob@example.com"),
]
def test_user_name(sample_user: User):
assert sample_user.name == "Alice"Scoped Fixtures
@pytest.fixture(scope="session")
async def db() -> AsyncIterator[Database]:
database = Database("postgresql://localhost/test")
await database.connect()
yield database
await database.disconnect()
@pytest.fixture(autouse=True)
async def clean_db(db: Database) -> AsyncIterator[None]:
yield
await db.execute("DELETE FROM users")Async Testing
import pytest
@pytest.mark.asyncio
async def test_fetch_users(db: Database):
users = await db.fetch_all("SELECT * FROM users")
assert len(users) == 0
@pytest.mark.asyncio
async def test_create_user(db: Database):
await db.execute("INSERT INTO users (name) VALUES (:name)", {"name": "Alice"})
users = await db.fetch_all("SELECT * FROM users")
assert len(users) == 1Parametrize
@pytest.mark.parametrize(
("input_val", "expected"),
[
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
],
)
def test_uppercase(input_val: str, expected: str):
assert input_val.upper() == expected
@pytest.mark.parametrize(
("a", "b", "expected"),
[
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
],
ids=["positive", "zeros", "negative"],
)
def test_add(a: int, b: int, expected: int):
assert a + b == expectedMocking
from unittest.mock import AsyncMock, patch
@pytest.mark.asyncio
async def test_send_email():
with patch("my_app.email.send", new_callable=AsyncMock) as mock_send:
mock_send.return_value = True
result = await notify_user("alice@example.com", "Hello")
assert result is True
mock_send.assert_called_once_with("alice@example.com", "Hello")FastAPI Testing
import pytest
from httpx import ASGITransport, AsyncClient
from my_api.main import app
@pytest.fixture
async def client() -> AsyncIterator[AsyncClient]:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest.mark.asyncio
async def test_list_users(client: AsyncClient):
response = await client.get("/users/")
assert response.status_code == 200
assert isinstance(response.json(), list)
@pytest.mark.asyncio
async def test_create_user(client: AsyncClient):
response = await client.post("/users/", json={"name": "Alice", "email": "alice@example.com"})
assert response.status_code == 201
assert response.json()["name"] == "Alice"Exception Testing
def test_raises_value_error():
with pytest.raises(ValueError, match="Invalid email"):
validate_email("not-an-email")
@pytest.mark.asyncio
async def test_raises_not_found():
with pytest.raises(HTTPException) as exc_info:
await get_user(999)
assert exc_info.value.status_code == 404Coverage
uv run pytest --cov=src --cov-report=term-missing
uv run pytest --cov=src --cov-report=html[tool.coverage.run]
source = ["src"]
omit = ["tests/*"]
[tool.coverage.report]
fail_under = 80
show_missing = trueRunning Tests
uv run pytest
uv run pytest tests/test_users.py
uv run pytest -k "test_create"
uv run pytest -x
uv run pytest --tb=short -qType Checking
Setup
uv add --dev mypy pyrightmypy Configuration
[tool.mypy]
strict = true
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = falseuv run mypy src/pyright Configuration
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"
reportMissingTypeStubs = falseuv run pyright src/Common Type Patterns
Function Signatures
def greet(name: str) -> str:
return f"Hello, {name}"
def find_user(user_id: int) -> User | None:
...
async def fetch_users(limit: int = 10) -> list[User]:
...Collections
from collections.abc import Mapping, Sequence
def process_items(items: Sequence[str]) -> None:
...
def merge_configs(base: Mapping[str, int], override: Mapping[str, int]) -> dict[str, int]:
return {**base, **override}Callables
from collections.abc import Callable, Awaitable
def retry(func: Callable[[], Awaitable[str]], times: int = 3) -> Callable[[], Awaitable[str]]:
...
Handler = Callable[[Request], Awaitable[Response]]TypeVar and Generics
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else NoneNew-Style Generics (Python 3.12+)
def first[T](items: list[T]) -> T | None:
return items[0] if items else None
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()TypedDict
from typing import NotRequired, TypedDict
class UserDict(TypedDict):
id: int
name: str
email: str
bio: NotRequired[str]
def format_user(user: UserDict) -> str:
return f"{user['name']} <{user['email']}>"Literal Types
from typing import Literal
def set_log_level(level: Literal["debug", "info", "warning", "error"]) -> None:
...Protocol (Structural Typing)
from typing import Protocol, runtime_checkable
@runtime_checkable
class Renderable(Protocol):
def render(self) -> str: ...
class HTMLElement:
def render(self) -> str:
return "<div></div>"
def display(item: Renderable) -> None:
print(item.render())Annotated Types
from typing import Annotated
from pydantic import Field
PositiveInt = Annotated[int, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]Type Aliases
type UserID = int
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
type Handler = Callable[[Request], Awaitable[Response]]Overloaded Functions
from typing import overload
@overload
def parse(data: str) -> dict[str, str]: ...
@overload
def parse(data: bytes) -> dict[str, bytes]: ...
def parse(data: str | bytes) -> dict[str, str] | dict[str, bytes]:
if isinstance(data, str):
return {"value": data}
return {"value": data}Running Type Checks
uv run mypy src/
uv run pyright src/
uvx mypy src/
uvx pyright src/py.typed Marker
For libraries, include a py.typed marker file:
src/
my_lib/
__init__.py
py.typeduv init --lib my-libThe --lib flag creates this automatically.
Workspaces
Workspace Structure
my-monorepo/
pyproject.toml # Root workspace config
uv.lock # Single lockfile for all packages
packages/
shared/
pyproject.toml
src/shared/
__init__.py
api/
pyproject.toml
src/api/
__init__.py
worker/
pyproject.toml
src/worker/
__init__.pyRoot pyproject.toml
[project]
name = "my-monorepo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[tool.uv.workspace]
members = ["packages/*"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Exclude Members
[tool.uv.workspace]
members = ["packages/*"]
exclude = ["packages/experimental"]Member pyproject.toml
Shared Library
[project]
name = "shared"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"API Service (depends on shared)
[project]
name = "api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn>=0.34.0",
"shared",
]
[tool.uv.sources]
shared = { workspace = true }
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Worker Service (depends on shared)
[project]
name = "worker"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"celery>=5.4",
"shared",
]
[tool.uv.sources]
shared = { workspace = true }
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Workspace Commands
Lock All Packages
uv lockA single uv.lock at the workspace root covers all members.
Sync Specific Package
uv sync --package api
uv sync --package workerRun in Specific Package
uv run --package api uvicorn api:app --reload
uv run --package worker celery -A worker.tasks worker
uv run --package shared pytestBuild Specific Package
uv build --package shared
uv build --package apiWorkspace Dependency Resolution
All workspace members share a single lockfile. Dependencies are resolved together, preventing version conflicts across packages.
[tool.uv.sources]
shared = { workspace = true }The workspace = true source tells uv to resolve the dependency from the workspace rather than PyPI.
Development Workflow
Adding Dependencies to a Member
cd packages/api
uv add httpxOr from the root:
uv add --package api httpxRunning Tests Across Workspace
uv run --package shared pytest
uv run --package api pytest
uv run --package worker pytestShared Dev Dependencies at Root
[dependency-groups]
dev = [
"pytest>=8.0",
"ruff>=0.8.0",
"mypy>=1.13",
]Root dev dependencies are available to all workspace members when synced.