uv and Poetry replace the pip + venv + pip-tools + build + twine combination with one tool that manages the virtual environment, resolves and locks dependencies, builds and publishes. uv (from Astral, written in Rust) has become the fast default for new projects; Poetry remains common in existing codebases.
Bandit, Semgrep, CodeQL, SonarQube Community Build
SAST (commercial)
Checkmarx One, Veracode, Snyk Code, SonarQube Server
SCA
pip-audit (on an exported requirements file), OSV-Scanner (reads uv.lock / poetry.lock), Trivy, Snyk, Mend
Frameworks
FastAPI, Django, Flask
Test and coverage
pytest, pytest-cov
Build output
wheel (.whl), sdist (.tar.gz)
CI cache
~/.cache/uv, ~/.cache/pypoetry
How the build works
In CI, each stage of the generic build pipeline maps to a uv / Poetry command, plugin or scanner. Press play, or step through it:
Source + lockfile: A commit triggers CI. pyproject.toml and a universal lockfile (uv.lock or poetry.lock) pin every dependency for every platform.
Sync the environment: uv python install reads .python-version, then uv sync --locked (or poetry sync) makes .venv match the lock and fails if it is stale.
Lint and format: uv run ruff check . and uv run ruff format --check . lint the code and check formatting.
Type check: uv run mypy src checks type annotations before any test runs.
Test and coverage: uv run pytest --cov=src --cov-fail-under=80 runs the tests and fails below 80% line coverage.
Security scans in parallel: uv run bandit checks our code, uv export plus uvx pip-audit checks the locked dependencies (OSV-Scanner reads uv.lock directly), and Gitleaks scans for secrets.
Quality gate: Lint, types, tests with the coverage gate and both scanners must pass before anything is built.
Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
Build the distributions (pass): uv build (or poetry build) writes the wheel and sdist to dist/.
SBOM and signing (pass): uvx --from cyclonedx-bom cyclonedx-py environment .venv writes sbom.json, and the distributions are signed with Sigstore cosign.
Publish (pass): uv publish (or poetry publish) uploads dist/ to PyPI or a private index.
The lockfile is universal: it records versions for every platform and Python version, so the same lock works on developer laptops and Linux CI runners.
sync makes the environment match the lock exactly, removing anything extra.
uv can also install Python itself (uv python install), which removes the need for pyenv on build agents.
uv sync--locked# reproducible install (fails on stale lock)
uv run ruff check .&& uv run ruff format --check.# lint + format
uv run mypy src # type check
uv run bandit -r src -ll# SAST
uv export--format requirements-txt --no-hashes-o req.txt && uvx pip-audit -r req.txt # SCA
uv run pytest --cov=src --cov-fail-under=80 # tests + coverage gate
uvx --from cyclonedx-bom cyclonedx-py environment .venv -o sbom.json # SBOM
uv build # wheel + sdist
Minimal project
1
2
3
4
5
6
7
8
9
10
11
12
13
# pyproject.toml for uv[project]name="demo"version="1.0.0"requires-python=">=3.12"dependencies=["fastapi[standard]>=0.115"][dependency-groups]dev=["pytest","pytest-cov","ruff","mypy","bandit"][build-system]requires=["hatchling"]build-backend="hatchling.build"
name:python-uv-cion:[push,pull_request]permissions:contents:readjobs:build:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v4-uses:astral-sh/setup-uv@v5with:enable-cache:true# caches ~/.cache/uv keyed on uv.lock-run:uv python install# reads .python-version-run:uv sync --locked-run:uv run ruff check . && uv run ruff format --check .-run:uv run bandit -r src -ll-run:uv export --format requirements-txt --no-hashes -o req.txt && uvx pip-audit -r req.txt-run:uv run pytest --cov=src --cov-fail-under=80-run:uv build-uses:actions/upload-artifact@v4with:name:distpath:dist/
For Poetry, replace the uv steps with pipx install poetry, poetry sync, poetry run pytest and poetry build, and cache ~/.cache/pypoetry with actions/setup-python (cache: poetry).
Best practices
Commit the lockfile and use uv sync --locked / poetry sync in CI so a stale lock fails.
Use dependency groups for dev, test and lint tools instead of separate requirements files.
Pin Python with .python-version, and let uv install it on runners.
Use uvx for one-off tools (pip-audit, cyclonedx) so they do not end up in the project lock.
In containers, copy pyproject.toml and the lockfile first, run uv sync --locked --no-dev, then copy the source, so the dependency layer stays cached.
Point both tools at your private index ([[tool.uv.index]] or poetry source add) instead of PyPI directly.