Packages & libraries
Python projects with uv 0.12 (Astral's all-in-one tool: Python installs, venvs, locking, running,
building, publishing): virtual environments, pyproject.toml, layouts, imports, lockfiles, tooling
config and the go-to library for each job. Targets Python 3.14. Tests are in Testing; the
TypeScript equivalent is Modules & packages.
uv at a glance
curl -LsSf https://astral.sh/uv/install.sh | sh # or:
brew install uv
uv self update # standalone installs only| Command | Does |
|---|---|
uv init | new project: pyproject.toml, .python-version, README, git |
uv add httpx / uv remove httpx | edit dependencies, re-lock, sync .venv |
uv sync | make .venv match uv.lock exactly (creates both if missing) |
uv lock | resolve and write uv.lock without installing |
uv run CMD | lock + sync if stale, then run CMD inside .venv |
uv tree | dependency tree; --outdated shows newer versions |
uv version --bump minor | bump project.version (major, minor, patch, alpha, rc, …) |
uv format | run the Ruff formatter over the project |
uv check | type-check with ty (experimental in 0.12) |
uv audit | scan locked deps for known vulnerabilities (experimental) |
uv export | uv.lock to requirements.txt, pylock.toml or CycloneDX |
uv build / uv publish | sdist + wheel into dist/; upload to PyPI |
uv tool install / uvx | global CLI tools / run a tool once |
uv python install | download a managed CPython or PyPy |
uv pip …, uv venv | pip-compatible low-level interface |
There's no source .venv/bin/activate step: uv run finds the project, checks the lock is fresh and
runs in its venv. Activate only if you want bare python/pytest in your shell. What a venv is and
how to drive one by hand is in Virtual environments.
Python versions
uv python install 3.14 # managed CPython
uv python install 3.14t # free-threaded (no GIL) build
uv python install --default # python, python3 on PATH
uv python pin 3.14 # writes .python-version
uv python list --only-installed
uv python upgrade 3.14 # latest patch release
uv run -p 3.13 pytest # one run on another version| File or field | Role |
|---|---|
.python-version | the interpreter uv uses for this project's .venv (like .nvmrc) |
requires-python = ">=3.14" | versions the project supports; the lock resolves for all of them |
uv python find | which interpreter uv would pick here |
uv downloads a missing interpreter on first use, so a fresh clone needs only uv sync.
Virtual environments
A virtual environment (venv) is a folder with its own python and its own site-packages, so
each project's packages are kept apart from every other project's and from the operating system's
Python. It is not a virtual machine or a container: it reuses an installed interpreter (a symlink, or
a copy on Windows) and only isolates installed packages. Python has one version of each package per
environment, so two projects that need different versions of the same library each need their own.
.venv/pyvenv.cfg # home = base interpreter's dirbin/python # -> python3.14 (a symlink)pipactivate # for bash and zshactivate.fishlib/python3.14/site-packages/ # what pip installs lands hereOn Windows the folders are .venv\Scripts\ (with python.exe, activate.bat, Activate.ps1) and
.venv\Lib\site-packages\.
Create, activate, use
cd myproject
python3 -m venv .venv # create it (standard library)
source .venv/bin/activate # bash/zsh; prompt: (.venv)
# fish: source .venv/bin/activate.fish
which python # …/myproject/.venv/bin/python
python -m pip install httpx # installs into .venv only
python -m pip list
deactivate # back to the outer pythoncd myproject
py -m venv .venv # py: the Python launcher
.venv\Scripts\Activate.ps1 # cmd.exe: activate.bat
python -m pip install httpx
Get-Command python # …\.venv\Scripts\python.exe
deactivateIf PowerShell refuses to run Activate.ps1, allow local scripts once:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.
python -m venv option | Does |
|---|---|
--prompt NAME | label shown in the shell prompt instead of the folder name |
--upgrade-deps | upgrade pip in the new venv to the latest release |
--clear | empty an existing venv folder and start over |
--system-site-packages | also see the base interpreter's packages (rarely what you want) |
--without-pip | no pip at all (uv-managed venvs work this way) |
--upgrade | re-point the venv after the base Python was upgraded in place (patch releases) |
What activation does
Activation is a convenience, nothing more: it puts .venv/bin (or .venv\Scripts) first on PATH,
sets VIRTUAL_ENV, adds (.venv) to the prompt and defines deactivate. Running the venv's
interpreter by path does the same job without it, which is what you want in scripts, cron jobs,
systemd units and Dockerfiles:
.venv/bin/python -m pytest # no activation needed
/srv/app/.venv/bin/python app.py # absolute pathAm I in the right one?
python -c "import sys; print(sys.prefix)" # venv folder
python -c "import sys; print(sys.prefix != sys.base_prefix)"
echo "$VIRTUAL_ENV" # set by activate
python -m pip -V # pip's site-packages pathsys.prefix != sys.base_prefix is True inside any venv. Always install with python -m pip
rather than bare pip: it guarantees the pip belongs to the python you are about to run.
With uv
uv venv # .venv, uv picks the Python
uv venv --python 3.14 # a specific version
uv venv --seed # also put pip in it
uv pip install httpx # into .venv, no activation
uv pip install -r requirements.txt
uv run python app.py # uses .venv automaticallyIn a uv project you rarely create a venv yourself: uv sync, uv add and uv run create .venv
and keep it matching uv.lock. A uv venv has no pip unless you pass --seed; use uv pip instead.
Rules of thumb
| Do | Why |
|---|---|
One venv per project, named .venv, in the project root | VS Code, PyCharm and uv find it on their own |
Keep it out of git (.venv/ in .gitignore) | it is machine-specific; rebuild it from the lockfile or requirements.txt. Python 3.13+ also writes a .gitignore inside new venvs |
| Delete and recreate instead of repairing | rm -rf .venv and reinstall takes seconds and fixes most mysteries |
| Don't move or rename a venv folder | absolute paths are written into its scripts and activate |
| Recreate it after a Python minor upgrade (3.13 → 3.14) | the venv points at the old interpreter, and compiled packages target its ABI |
Never sudo pip install into the system Python | it can break OS tools; most Linux distributions now refuse it outright (PEP 668) |
Install command-line tools with uv tool install or pipx | each tool gets its own venv, and the tool lands on PATH |
Editors and notebooks
- VS Code:
Python: Select Interpreterfrom the command palette, then pick.venv; new terminals then activate it automatically. - PyCharm: Settings → Project → Python Interpreter → Add → existing environment, choose
.venv. - Jupyter: give the notebook server a kernel that runs in the venv:
python -m pip install ipykernel, thenpython -m ipykernel install --user --name myproject, and choosemyprojectas the kernel.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
error: externally-managed-environment | the OS Python is marked as managed by the package manager (PEP 668) | make a venv and install there; uv tool or pipx for CLIs |
ModuleNotFoundError although you installed it | it went into a different interpreter | compare which python with python -m pip -V; activate or select .venv in the editor |
ensurepip is not available when creating a venv | Debian and Ubuntu ship venv separately | sudo apt install python3-venv (or python3.14-venv) |
bad interpreter: No such file or directory | the venv was moved, or its base Python was removed or upgraded | delete .venv and recreate it |
pip still runs the system pip after activating | the shell cached the old path | hash -r (bash) or rehash (zsh), or use python -m pip |
Activate.ps1 cannot be loaded | PowerShell's execution policy | Set-ExecutionPolicy -Scope CurrentUser RemoteSigned |
| Tool | What it manages |
|---|---|
venv | standard library; makes the environment only |
| virtualenv | the older third-party tool venv was based on; more options, faster creation |
| uv | Python versions, venvs, locking and installs (this sheet) |
| pyenv | Python versions only; pair it with venv |
| conda / mamba | environments that also hold non-Python libraries (CUDA, GDAL); its own package channels |
| Poetry, PDM, Hatch | project managers that create and manage a venv per project |
Project workflow
uv init shop # packaged app, src/ layout
uv init --lib shop # library (adds py.typed)
uv init --no-package scripts # flat main.py, no build
cd shop
uv add httpx "pydantic>=2.11" # [project] dependencies
uv add --dev pytest ruff # dependency group "dev"
uv add --group docs mkdocs # another group
uv add --optional cli typer # extra: pip install shop[cli]
uv add "git+https://github.com/org/lib@v1.2.0"
uv add --editable ../shared # local path dependency
uv add -r requirements.txt # import an old project
uv remove httpx
uv lock --upgrade-package httpx # bump one dep in the lock
uv lock --upgrade # bump everything allowed
uv sync --locked # CI: fail if lock is stale
uv run python -m shop
uv run --with rich python # extra package, this run only
uv run --env-file .env uvicorn app:appuv sync / uv run flag | Effect |
|---|---|
--locked | error if uv.lock doesn't match pyproject.toml (use in CI) |
--frozen | use uv.lock as is, never re-lock |
--no-dev | skip the dev group (production images) |
--group NAME / --all-groups | add groups on top of the defaults |
--extra NAME / --all-extras | install optional dependencies |
--no-install-project | deps only; good for a cached Docker layer |
--inexact | keep packages that aren't in the lock (sync is exact by default) |
--all-packages / --package NAME | workspace: every member / one member |
uv add writes a lower bound (httpx>=0.28.1) by default; --bounds major writes a caret-style
range instead (rich>=15.0.0,<16.0.0, or httpx>=0.28.1,<0.29.0 for 0.x). The lock pins exact versions and hashes either way.
Tools & scripts
uvx ruff check . # run a tool in a cached env
uvx ruff@0.16.9 check . # a specific version
uvx --from httpie http GET example.com # package ≠ command
uv tool install ruff # put ruff on PATH for good
uv tool list
uv tool upgrade --all
uv tool install -e . # your own CLI, editableuvx is uv tool run. Tools get their own isolated venvs, so they never clash with a project; to use a
tool that must import your code (pytest, mypy), add it to the project's dev group and uv run it.
Scripts with inline metadata (PEP 723)
A single .py file declares its own Python and dependencies in a comment block; uv run builds a
cached env for it. No project needed.
uv init --script tool.py --python 3.14
uv add --script tool.py httpx rich # edits the header
uv run tool.py
uv lock --script tool.py # tool.py.lock beside itpip & venv equivalents
| Task | uv project | uv pip (drop-in) | pip / venv |
|---|---|---|---|
| create venv | automatic (.venv) | uv venv | python -m venv .venv |
| install a package | uv add httpx | uv pip install httpx | pip install httpx |
| from a file | uv add -r requirements.txt | uv pip install -r requirements.txt | pip install -r requirements.txt |
| editable install | uv sync (project is editable) | uv pip install -e . | pip install -e . |
| lock | uv lock | uv pip compile requirements.in -o requirements.txt | pip-compile (pip-tools), pip lock |
| install exactly the lock | uv sync | uv pip sync requirements.txt | pip-sync |
| list / freeze | uv tree / uv export | uv pip list / uv pip freeze | pip list / pip freeze |
| uninstall | uv remove httpx | uv pip uninstall httpx | pip uninstall httpx |
| run a tool once | uvx black | pipx run black | |
| install a tool globally | uv tool install black | pipx install black |
uv pip works on whatever venv is active (or .venv) and never touches pyproject.toml; use it for
legacy requirements.txt repos. Prefer the project commands for anything new.
Coming from Bun & pnpm
| Bun / pnpm / npm | uv / Python |
|---|---|
package.json | pyproject.toml |
bun.lock, pnpm-lock.yaml | uv.lock |
node_modules/ | .venv/ (one per project; site-packages inside) |
bun install, pnpm install | uv sync |
bun add zod | uv add pydantic |
bun add -d vitest / devDependencies | uv add --dev pytest / [dependency-groups] |
optionalDependencies | extras ([project.optional-dependencies]), chosen by the installer: shop[cli] |
bun update zod | uv lock --upgrade-package pydantic |
bun outdated | uv tree --outdated |
bun run dev ("scripts") | uv run CMD; no scripts table, so use [project.scripts], a Makefile or just |
bunx, pnpm dlx, npx | uvx |
bun add -g | uv tool install |
"bin" | [project.scripts] |
.nvmrc, "engines" | .python-version, requires-python |
pnpm-workspace.yaml, "workspace:*" | [tool.uv.workspace], { workspace = true } |
.npmrc registry | [[tool.uv.index]] |
npm publish | uv build && uv publish |
| npm registry | PyPI |
^1.2.3 | >=1.2.3,<2 |
~1.2.3 | ~=1.2.3 |
| ESLint + Prettier | Ruff (ruff check, ruff format) |
tsc --noEmit / tsconfig.json | mypy, Pyright or ty / [tool.mypy], [tool.pyright] |
The big difference: Python has one version of each package per environment. There's no nested
node_modules, so two dependencies that need incompatible versions of a third can't be installed together.
pyproject.toml
[project]
name = "shop" # PyPI name; import name is shop
version = "0.1.0"
description = "Carts and invoices"
readme = "README.md"
requires-python = ">=3.14"
license = "MIT" # SPDX expression
authors = [{ name = "Ada", email = "ada@example.com" }]
dependencies = [
"httpx>=0.28",
"pydantic>=2.11,<3",
"uvloop>=0.21; sys_platform != 'win32'", # marker
]
[project.optional-dependencies] # extras: shop[cli]
cli = ["typer>=0.27"]
[project.scripts] # console commands
shop = "shop.cli:app"
[project.entry-points."pytest11"] # plugin hooks
shop = "shop.pytest_plugin"
[project.urls]
Homepage = "https://github.com/acme/shop"
[dependency-groups] # dev-only, never published
dev = ["pytest>=9", "ruff>=0.16"]
docs = ["mkdocs-material>=9"]
ci = [{ include-group = "dev" }, "pytest-cov>=7"]
[build-system]
requires = ["uv_build>=0.12.19,<0.13"]
build-backend = "uv_build"
[tool.uv]
default-groups = ["dev", "docs"] # synced by default| Table | Holds | Published? |
|---|---|---|
[project] | name, version, requires-python, runtime dependencies | yes (package metadata) |
[project.optional-dependencies] | extras users opt into: pip install "shop[cli]" | yes |
[dependency-groups] | dev, test, docs deps (PEP 735); include-group composes them | no |
[project.scripts] | name = "module:callable" commands created on install | yes |
[project.entry-points."group"] | plugin registration other packages discover | yes |
[build-system] | backend that turns the source into a wheel | used at build time |
[tool.*] | per-tool config: uv, ruff, mypy, pytest, coverage | no |
Build backends: uv_build (uv's default, pure Python only), hatchling, setuptools, flit_core,
pdm-backend, and maturin or scikit-build-core for Rust or C extensions. Pick one with
uv init --build-backend hatch. A project without [build-system] is never installed as a package:
fine for an app you only uv run.
Versions & lockfiles
| Specifier | Means | npm |
|---|---|---|
==1.4.2 | exactly this version | 1.4.2 |
>=1.4,<2 | range | ^1.4.0 |
~=1.4.2 | >=1.4.2, ==1.4.* (compatible release) | ~1.4.2 |
~=1.4 | >=1.4, ==1.* | ^1.4 |
==1.4.* | any 1.4.x | 1.4.x |
!=1.5.0 | exclude a broken release | |
pkg[extra]>=1 | with the package's extra | |
pkg; sys_platform == 'linux' | environment marker (also python_version, platform_machine) | |
pkg @ git+https://…@tag | direct URL (not allowed on PyPI uploads) |
Libraries: wide ranges with lower bounds, upper bounds only for known breaks. Apps: anything goes, because the lockfile pins exact versions.
| Lockfile | Made by | Notes |
|---|---|---|
uv.lock | uv lock (auto on add/run/sync) | universal: one file for every OS, Python version, extra and group; commit it |
pylock.toml | uv export --format pylock.toml, pip lock | PEP 751 standard (accepted 2025); pip 26.1+ installs it experimentally with pip install -r pylock.toml; uv pip install -r pylock.toml works too |
requirements.txt | uv export --format requirements.txt, uv pip compile | for tools and hosts that only read pip format; add --no-hashes if they choke on hashes |
Keep uv.lock as the source of truth; export the others in CI when something downstream needs them.
Layout & imports
shop/pyproject.tomluv.lock.python-versionsrc/shop/ # import shop__init__.py__main__.py # python -m shoppy.typed # ships type hintscart.py # shop.cartpayments/__init__.pystripe.py # shop.payments.stripetests/test_cart.pyshop/pyproject.tomlshop/ # importable from the repo root__init__.pycart.pytests/| src layout | Flat layout | |
|---|---|---|
| Import works | only after install (uv sync does an editable install) | from the repo root, installed or not |
| Tests run against | the installed package, as users get it | whatever is on disk, which can hide packaging bugs |
| Good for | libraries, anything published, most apps | scripts, notebooks, tiny apps |
| uv_build | default | set module-root = "" under [tool.uv.build-backend] |
| Term | What it is |
|---|---|
| module | one .py file; import name = file name |
| package | a directory with __init__.py; can hold modules and subpackages |
| namespace package | a directory with no __init__.py (PEP 420); several distributions can share it (acme.core, acme.api) |
| distribution | what you install from PyPI (pip install scikit-learn); its import name can differ (import sklearn) |
__init__.py | runs on first import of the package; re-export the public API here |
__main__.py | runs on python -m shop |
py.typed | empty marker file: the package ships inline type hints (PEP 561) |
"""Public API: callers import from shop, not submodules."""
from shop.cart import Cart, Line
__all__ = ["Cart", "Line"] # what `from shop import *` getsfrom shop.cart import Cart # absolute: the default choice
from ..cart import Line # relative: up one package
from .errors import PaymentError # same package
def charge(cart: Cart) -> Line:
if not cart.lines:
raise PaymentError("empty cart")
return cart.lines[-1]import sys
from shop.cli import main
sys.exit(main())| Import rule | Detail |
|---|---|
| Prefer absolute imports | from shop.cart import Cart; relative ones (., ..) only work inside a package |
Run package modules with -m | python src/shop/cart.py breaks relative imports; use uv run python -m shop.cart |
| Imports run once | the module is cached in sys.modules; top-level code runs on first import only |
| Guard script code | if __name__ == "__main__": keeps it from running on import |
| Break import cycles | move shared code to a third module, import inside the function, or import under if TYPE_CHECKING: for type-only use |
| Annotations are lazy (3.14) | PEP 649: annotations are evaluated on demand, so TYPE_CHECKING imports work in hints without quotes or from __future__ import annotations |
| Package data | importlib.resources.files("shop") / "data.json", not paths built from __file__ |
Workspaces
Several packages in one repo sharing one uv.lock and one .venv, like a pnpm monorepo
(see pnpm & monorepos).
acme/pyproject.toml # [tool.uv.workspace]uv.lock # one lock for all membersapps/api/pyproject.toml # depends on coresrc/packages/core/pyproject.tomlsrc/[tool.uv.workspace]
members = ["apps/*", "packages/*"][project]
name = "api"
dependencies = ["core"]
[tool.uv.sources]
core = { workspace = true } # use the local memberuv init apps/api # joins the workspace
uv add --package api core # writes both lines above
uv sync --all-packages
uv run --package api pytest
uv workspace listEvery member shares one resolution, so they can't pin conflicting versions of a dependency. When they
must, keep them as separate projects linked by path: { path = "../core", editable = true }.
Building & publishing
uv build # dist/*.tar.gz + *.whl
uv build --package core # one workspace member
uv version --bump patch
uv publish # token or trusted publishing
uv publish --index testpypi # index with publish-url| Artifact | What |
|---|---|
sdist (.tar.gz) | source plus pyproject.toml; the installer builds it |
wheel (.whl) | pre-built zip installed by copying; pure Python is py3-none-any |
Trusted publishing: PyPI trusts a GitHub Actions workflow through OIDC, so there's no API token to store or leak. On PyPI, add a publisher (repo, workflow file, environment); then:
name: release
on:
push:
tags: ["v*"]
jobs:
pypi:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # OIDC token for PyPI
contents: read
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10
- run: uv build
- run: uv publish # detects trusted publishingLint, format & types
[tool.ruff]
line-length = 88 # target-version from requires-python
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
[tool.mypy]
strict = true
files = ["src", "tests"]
[tool.pyright]
typeCheckingMode = "strict"
[tool.ty.rules]
possibly-unresolved-reference = "error"| Tool | Job | Run | Status |
|---|---|---|---|
| Ruff | linter + formatter + import sorter (replaces flake8, isort, Black) | uvx ruff check --fix, uv format | stable |
| mypy | reference type checker | uv run mypy | stable (2.x) |
| Pyright | fast checker; powers Pylance in VS Code | uv run pyright | stable |
| ty | Astral's Rust type checker and language server | uvx ty check, uv check | beta (0.0.x); fast, still filling gaps |
Run type checkers from the project env (uv run, dev group) so they see your dependencies. Ruff
doesn't need them, so uvx is fine. Pick one type checker for CI; mypy or Pyright if you want
no surprises, ty if speed matters and you can live with rough edges.
Go-to libraries
| Job | Pick | Notes |
|---|---|---|
| HTTP client | httpx | sync + async, HTTP/2; httpx2 is Pydantic's maintained continuation |
| Validation, models | pydantic | typed models, JSON (de)serialization |
| Web APIs | fastapi | pydantic + type hints; FastAPI |
| CLI | typer (or click) | typer builds on click from type hints |
| Arrays | numpy | NumPy |
| DataFrames | pandas or polars | pandas; polars is faster and lazy |
| Classic ML | scikit-learn | scikit-learn |
| Deep learning | pytorch | PyTorch |
| Testing | pytest, hypothesis | Testing |
| Lint, format | ruff | one tool for both |
| Type checking | mypy or pyright (ty coming) | see above |
| Settings, env vars | pydantic-settings | typed .env and env var loading |
| Dates, time zones | stdlib datetime + zoneinfo, or whenever | whenever makes DST mistakes hard |
| Logging | structlog | structured key-value logs; stdlib logging underneath |
| SQL | sqlalchemy + psycopg | psycopg 3 is the Postgres driver; see Postgres |
| Async | anyio | structured concurrency over asyncio or trio |
Recipes
New app project
When starting a service or CLI you'll run but not publish.
uv init --app shop --python 3.14
cd shop
uv add fastapi uvicorn pydantic-settings
uv add --dev pytest ruff mypy
# write src/shop/main.py with app = FastAPI()
uv run uvicorn shop.main:app --reload
git add pyproject.toml uv.lock .python-versionNew library with src layout
When the code will be published or shared between projects.
uv init --lib acme-geo # import name: acme_geo
cd acme-geo
uv add "httpx>=0.28" # keep lower bounds loose
uv add --dev pytest mypy ruff
uv run pytest && uv run mypy src
uv build && ls distAdd a CLI entry point
When the project should install a command, like "bin" in package.json.
[project.scripts]
acme = "acme_cli.cli:app" # module:callablefrom typing import Annotated
import typer
app = typer.Typer(no_args_is_help=True)
@app.command()
def greet(
name: str,
shout: Annotated[bool, typer.Option("--shout")] = False,
) -> None:
"""Say hello."""
msg = f"Hello, {name}!"
typer.echo(msg.upper() if shout else msg)
@app.command()
def version() -> None:
typer.echo("0.1.0")uv run acme greet Ada --shout in the project; uv tool install . puts acme on your PATH.
Pin Python per project
When one repo needs 3.14 and another is stuck on 3.12.
uv python pin 3.14 # .python-version, commit it
# pyproject.toml: requires-python = ">=3.14"
uv sync # rebuilds .venv on 3.14
uv run python --version
uv python pin --global 3.14 # default outside projectsSingle-file script
When a script needs a couple of packages and you don't want a project. exclude-newer makes re-runs
reproducible.
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.14"
# dependencies = ["httpx>=0.28", "rich>=14"]
# [tool.uv]
# exclude-newer = "2026-09-01T00:00:00Z"
# ///
import sys
import httpx
from rich import print
def stars(repo: str) -> int:
url = f"https://api.github.com/repos/{repo}"
r = httpx.get(url, timeout=10)
r.raise_for_status()
return int(r.json()["stargazers_count"])
if __name__ == "__main__":
repo = sys.argv[1] if sys.argv[1:] else "astral-sh/uv"
print(f"[bold]{repo}[/]: {stars(repo):,} stars")chmod +x stars.py && ./stars.py pola-rs/polars, or uv run stars.py.
Private index
When some packages come from a company registry (Artifactory, CodeArtifact, GitLab) and the rest from PyPI.
[[tool.uv.index]]
name = "internal"
url = "https://pkgs.example.com/simple/"
explicit = true # only for packages pinned below
[tool.uv.sources]
acme-auth = { index = "internal" }export UV_INDEX_INTERNAL_USERNAME=ci
export UV_INDEX_INTERNAL_PASSWORD="$TOKEN"
uv add acme-authexplicit = true stops a public package with the same name from being picked up (dependency confusion).
CI install with cache
When GitHub Actions should install exactly the lockfile, fast.
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.13", "3.14"]
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10
with:
enable-cache: true
python-version: ${{ matrix.python }}
- run: uv sync --locked --all-extras
- run: uv run ruff check
- run: uv run pytestFor Docker images see Dockerfile: copy uv.lock first,
uv sync --locked --no-dev --no-install-project, then copy the source.
References
- uv documentation (opens in a new tab): guides, concepts and the full CLI reference
- uv: Working on projects (opens in a new tab):
init,add,sync,run - uv: Managing dependencies (opens in a new tab): sources, groups, extras
- uv: Workspaces (opens in a new tab): members and shared locks
- uv: Package indexes (opens in a new tab): private registries and credentials
- uv: Running scripts (opens in a new tab): PEP 723 inline metadata
- uv: GitHub Actions (opens in a new tab): caching and publishing
- Python Packaging User Guide: Writing pyproject.toml (opens in a new tab): every
[project]field - Packaging guide: src layout vs flat layout (opens in a new tab)
- Packaging guide: Version specifiers (opens in a new tab)
- Packaging guide: pylock.toml (opens in a new tab): the PEP 751 format
- PyPI: Trusted publishers (opens in a new tab): OIDC publishing setup
- Python docs: The import system (opens in a new tab): packages, namespace packages,
__main__ - Ruff (opens in a new tab), mypy (opens in a new tab), Pyright (opens in a new tab), ty (opens in a new tab): tooling config