uv Python Packaging skill for Codex

One lockfile from laptop to Docker to PyPI. Codex chooses app, package or library layout up front, keeps dev tools in dependency groups instead of runtime deps, and installs with uv sync --locked everywhere so stale lockfiles fail CI. It handles private indexes without dependency confusion, splits Docker layers for fast rebuilds, and treats PyPI publishing as an irreversible step that needs your go-ahead.

  • Languages
  • $uv-python-packaging
  • 207 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- uv-python-packaging

Installs to ~/.agents/skills/uv-python-packaging/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $uv-python-packaging, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Sets up and maintains Python projects with uv - pyproject.toml metadata, uv.lock, dependency groups, Python version pinning, workspaces, private indexes, Docker and CI installs, building wheels and publishing to PyPI with trusted publishing. Use when a user asks to migrate from pip, Poetry or pip-tools to uv, add or upgrade dependencies, fix lockfile conflicts, set up uv in CI or a Dockerfile, or publish a package.

Use it when

  • Starting a Python project or migrating one from pip, pip-tools or Poetry
  • CI and local environments install different dependency versions
  • A package needs building and publishing to PyPI or a private index
  • Several Python packages in one repo should share a lockfile

Use something else when

  • The question is about packaging a command-line entry point and its UX: use python-cli-tools
  • You are orchestrating builds across a polyglot monorepo: use monorepo-tooling

How to check the result

A clean checkout passes uv sync --locked and the test suite, and for libraries the built wheel installs and imports in an isolated environment.

The mistake it prevents

Installing packages with pip into the uv-managed venv, which the next uv sync silently removes because they are not in uv.lock.

~/.agents/skills/uv-python-packaging/SKILL.md

--- name: uv-python-packaging description: Sets up and maintains Python projects with uv - pyproject.toml metadata, uv.lock, dependency groups, Python version pinning, workspaces, private… license: MIT ---

Done well means: pyproject.toml is the single source of dependency truth, uv.lock is committed and CI installs with --locked, every environment (laptop, CI, Docker) gets the same versions, and a release is a reproducible uv build plus an approved publish.

Before changing anything

  • Read AGENTS.md for the package manager and commands the team expects. Do not migrate a Poetry or pip-tools project to uv unless the user asked.
  • Inspect: pyproject.toml, uv.lock, .python-version, requirements*.txt, poetry.lock, Dockerfile, CI workflow files.
  • uv --version. Features below assume uv 0.8 or newer. If the repo pins one, respect [tool.uv] required-version.
  • Network: uv add, uv lock and uv sync resolve and download from indexes. In a Codex sandbox with network off, ask for approval, or use uv sync --offline when the cache is warm.

Project type decides the layout

GoalCommandResult
Application or service, never installed by othersuv init --appNo build system; the project itself is not installed as a package
Application with console scripts or tests importing the packageuv init --packagesrc/ layout, [build-system] with uv_build
Library to publishuv init --libsrc/ layout, py.typed, uv_build backend
Several packages in one repoworkspace (below)One shared uv.lock at the root

uv_build is the default backend for packaged projects. Keep hatchling or setuptools in existing projects; switching backends is a separate change with its own build verification.

Everyday commands

uv add httpx 'pydantic>=2.7'          # runtime dependency, updates pyproject + lock + venv
uv add --dev pytest ruff mypy         # [dependency-groups] dev (PEP 735)
uv add --group docs mkdocs-material   # any named group
uv add --optional cli typer           # [project.optional-dependencies] extra, for libraries
uv remove httpx
uv run pytest -q                      # syncs the env first, then runs
uv run --with ipython ipython         # one-off tool without adding it to the project
uv lock --upgrade-package httpx       # bump one package within its constraints
uv lock --upgrade                     # bump everything (separate PR)
uv tree --outdated --depth 1          # what can move
uv python pin 3.12                    # writes .python-version
uvx ruff check .                      # run a tool in an ephemeral env

Never edit uv.lock by hand. Never pip install into the project venv; the next uv sync removes anything not in the lock.

pyproject.toml shape

[project]
name = "acme-billing"
version = "1.4.0"
description = "Invoice generation for Acme"
readme = "README.md"
requires-python = ">=3.11"
license = "MIT"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.7,<3",
]

[project.optional-dependencies]
pdf = ["weasyprint>=62"]

[project.scripts]
acme-billing = "acme_billing.cli:app"

[dependency-groups]
dev = ["pytest>=8", "ruff", "mypy"]
docs = ["mkdocs-material"]

[build-system]
requires = ["uv_build>=0.11,<0.12"]  # keep the bounds uv init wrote; bump with uv itself
build-backend = "uv_build"

[tool.uv]
default-groups = ["dev"]

Rules:

  • Libraries: lower bounds only (>=), upper bound only for a known-breaking major (<3). Applications rely on the lockfile for exact pins.
  • Dev tools go in [dependency-groups], never in [project.dependencies]; groups are not published. Extras (optional-dependencies) are for features users opt into.
  • requires-python lower bound drives resolution: a too-low bound forces old package versions. Raise it to what you test.

Migration from other tools

FromSteps
requirements.txtuv init --app (or reuse pyproject), then uv add -r requirements.txt and uv add --dev -r requirements-dev.txt; delete the txt files once CI uses uv
pip-tools .in filesuv add -r requirements.in; compare uv export --format requirements-txt against the old .txt for version drift
PoetryMove [tool.poetry.dependencies] into [project] (caret ^1.2 becomes >=1.2,<2), dev groups into [dependency-groups], then uv lock. The migrate-to-uv tool automates this (uvx migrate-to-uv, needs network)

After migration, diff resolved versions (uv export --no-hashes vs the old lock) and run the full test suite before deleting the old files.

Private indexes and sources

[[tool.uv.index]]
name = "internal"
url = "https://pypi.internal.example.com/simple"
explicit = true                    # only packages pinned below come from here

[tool.uv.sources]
acme-auth = { index = "internal" }
acme-shared = { path = "../shared", editable = true }
torch = { index = "pytorch-cpu" }

Credentials come from environment variables (UV_INDEX_INTERNAL_USERNAME, UV_INDEX_INTERNAL_PASSWORD) or keyring, never from the URL in a committed file. Without explicit = true, uv may pick a same-named package from the first index that has it, which is the dependency-confusion risk.

Workspaces

# root pyproject.toml
[tool.uv.workspace]
members = ["packages/*"]

[tool.uv.sources]
acme-core = { workspace = true }

uv sync --all-packages installs every member; uv run --package acme-api pytest targets one. All members share one uv.lock and one requires-python intersection; if members need conflicting versions of a dependency, they cannot be one workspace.

CI

- uses: astral-sh/setup-uv@v6        # or the current major release
  with:
    enable-cache: true
- run: uv sync --locked --all-extras
- run: uv run pytest -q

--locked fails if uv.lock is out of date with pyproject.toml; that is the check that someone forgot to commit the lock. --frozen skips the check and is only for speed in trusted contexts.

Docker

FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv uv sync --locked --no-dev --no-install-project
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv uv sync --locked --no-dev
ENV PATH="/app/.venv/bin:$PATH"
CMD ["acme-billing"]

The first sync caches the dependency layer; source edits rebuild only the second. Pin the uv image tag to the version used in CI.

Build and publish

uv version --bump minor          # or set [project].version by hand
uv build                         # dist/*.whl and dist/*.tar.gz
uvx twine check dist/*           # metadata and README render check
uv run --isolated --with dist/*.whl python -c "import acme_billing"   # smoke test the wheel

Publishing is outward-facing and irreversible (a version number can never be reused on PyPI). Stop and get explicit user confirmation before running it. Prefer trusted publishing from CI (uv publish in a GitHub Actions job with id-token: write and a PyPI trusted publisher configured) over long-lived API tokens. Test first with uv publish --index testpypi when a testpypi index with publish-url is configured.

Gotchas

  • Committing .venv or omitting uv.lock from git defeats reproducibility.
  • uv pip install exists for legacy workflows, but it does not update pyproject.toml or the lock. Use uv add in uv projects.
  • A src/ package that is not installed (app mode) is not importable from tests unless the test runner sets pythonpath; use --package if tests import it.
  • Platform-specific wheels (torch, CUDA builds) need [tool.uv.sources] with markers or explicit indexes; resolution is universal across platforms by default.
  • uv run re-syncs before every run; in scripts that must not touch the env, use uv run --no-sync or call .venv/bin/python directly.

Verification

  • uv lock --check (or uv sync --locked) passes on a clean checkout.
  • uv sync --locked && uv run pytest passes in CI from an empty cache at least once.
  • For libraries: the built wheel installs into a clean env and imports; twine check passes.
  • git diff uv.lock on a dependency PR contains only the intended packages; anything else needs an explanation.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "uv Python Packaging"
  short_description: "One lockfile from laptop to Docker to PyPI"
  default_prompt: "$uv-python-packaging Move this project from requirements.txt to uv with a committed lockfile"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc