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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- uv-python-packaging Installs to ~/.agents/skills/uv-python-packaging/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install uv-python-packaging The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project uv-python-packaging Installs to .agents/skills/uv-python-packaging/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/uv-python-packaging.zipunzip -o uv-python-packaging.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) uv-python-packaging PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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.mdfor 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 lockanduv syncresolve and download from indexes. In a Codex sandbox with network off, ask for approval, or useuv sync --offlinewhen the cache is warm.
Project type decides the layout
| Goal | Command | Result |
|---|---|---|
| Application or service, never installed by others | uv init --app | No build system; the project itself is not installed as a package |
| Application with console scripts or tests importing the package | uv init --package | src/ layout, [build-system] with uv_build |
| Library to publish | uv init --lib | src/ layout, py.typed, uv_build backend |
| Several packages in one repo | workspace (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-pythonlower bound drives resolution: a too-low bound forces old package versions. Raise it to what you test.
Migration from other tools
| From | Steps |
|---|---|
requirements.txt | uv 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 files | uv add -r requirements.in; compare uv export --format requirements-txt against the old .txt for version drift |
| Poetry | Move [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
.venvor omittinguv.lockfrom git defeats reproducibility. uv pip installexists for legacy workflows, but it does not updatepyproject.tomlor the lock. Useuv addin uv projects.- A
src/package that is not installed (app mode) is not importable from tests unless the test runner setspythonpath; use--packageif 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 runre-syncs before every run; in scripts that must not touch the env, useuv run --no-syncor call.venv/bin/pythondirectly.
Verification
uv lock --check(oruv sync --locked) passes on a clean checkout.uv sync --locked && uv run pytestpasses in CI from an empty cache at least once.- For libraries: the built wheel installs into a clean env and imports;
twine checkpasses. git diff uv.lockon 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.yaml
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.