tox, nox, pre-commit & Docker
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the Python Reference bibliography. |
Poetry manages one environment per project; test runners, Git hooks and container builds bring their own environments. This
page shows how to make each of them install exactly what poetry.lock says, with Poetry 2.5.1, tox 4.67.1, nox 2026.10.20,
nox-poetry 1.2.0, pre-commit 4.7.0 and Docker 29.9.1. The examples ran on 2026-10-22 on Linux with CPython 3.13.16 and 3.14.6,
on the birdtally project, whose test, lint, docs and dev groups are PEP 735 [dependency-groups]. Scratch paths are
shortened to ….
tox
tox 4 builds the project through its PEP 517 back end, so it works with any Poetry project. The question is who installs the dependencies:
| Approach | Effect |
|---|---|
tox only ( |
tox builds an sdist and pip installs it, resolving the dependencies afresh: the lock is ignored. |
Poetry in |
Poetry installs the locked versions (and the project, editable) into the tox environment. Tests run against the source tree. |
Poetry in |
tox installs the built package first, then Poetry brings the dependencies to the locked versions. |
The second form, with poetry sync so that each environment holds the lock and nothing else:
[tox]
env_list = py313, py314
[testenv]
skip_install = true
allowlist_externals = poetry
commands_pre =
poetry sync --only main,test
commands =
poetry env info -p
poetry run pytest -q tests/
tox -r -e py314
py314: remove tox env folder …/birdtally/.tox/py314
py314: commands_pre[0]> poetry sync --only main,test
Installing dependencies from lock file
Package operations: 5 installs, 0 updates, 0 removals
- Installing iniconfig (2.3.1)
- Installing packaging (26.3)
- Installing pluggy (1.7.0)
- Installing pygments (2.21.0)
- Installing pytest (9.1.1)
Installing the current project: birdtally (0.1.0)
py314: commands[0]> poetry env info -p
…/birdtally/.tox/py314
py314: commands[1]> poetry run pytest -q tests/
.. [100%]
2 passed in 0.01s
py314: OK (3.36=setup[0.19]+cmd[1.20,0.66,1.31] seconds)
congratulations :) (3.38 seconds)
Poetry uses the tox environment because tox activates it (VIRTUAL_ENV); poetry env info -p proves it. Three traps:
-
--with testinstalls every group. PEP 735 groups and[tool.poetry.group]groups are non-optional unless markedoptional = true, so--withadds nothing to them: the first run of this example, withpoetry sync --with test, installed 22 packages including mkdocs and ruff. Use--only main,test. -
poetrymust be onPATHand listed inallowlist_externals, or tox refuses to run it. -
Credentials: tox passes only a few environment variables through. Add
passenv = POETRY_HTTP_BASIC_*(or the variables you use;DBUS_SESSION_BUS_ADDRESSfor a Linux desktop keyring), or Poetry cannot reach private sources.
The Poetry FAQ’s own examples still use poetry install --no-root --sync; --sync has been deprecated since 2.0 in favour of
poetry sync.
nox
The Poetry FAQ points to the third-party nox-poetry, whose session.install builds the project as a wheel and installs
dependencies constrained by poetry export. A plain nox session can also call poetry sync into the session’s virtual
environment:
import nox
from nox_poetry import session
@session(python=["3.13", "3.14"])
def tests(session):
session.install(".", "pytest")
session.run("pytest", "-q", "tests/")
@nox.session(python="3.14")
def tests_sync(session):
session.run_install(
"poetry", "sync", "--only", "main,test",
external=True,
env={"VIRTUAL_ENV": session.virtualenv.location},
)
session.run("pytest", "-q", "tests/")
nox > Running session tests-3.14
nox > Creating virtual environment (virtualenv) using python3 in .nox/tests-3-14
nox > …/python -m build …/birdtally --outdir …/birdtally/dist --wheel
…
nox > poetry export --format=requirements.txt --without-hashes
nox > python -m pip install --constraint=.nox/tests-3-14/tmp/requirements.txt file://…/birdtally/dist/birdtally-0.1.0-py3-none-any.whl pytest
nox > pytest -q tests/
.. [100%]
2 passed in 0.01s
nox > Session tests-3.14 was successful in 6 seconds.
nox > Running session tests_sync
nox > Creating virtual environment (virtualenv) using python3 in .nox/tests_sync
nox > poetry sync --only main,test
Installing dependencies from lock file
Package operations: 5 installs, 0 updates, 0 removals
…
Installing the current project: birdtally (0.1.0)
nox > pytest -q tests/
.. [100%]
2 passed in 0.01s
nox > Ran 3 sessions in 15 seconds:
nox > * tests-3.13: success, took 7 seconds
nox > * tests-3.14: success, took 6 seconds
nox > * tests_sync: success, took 2 seconds
Look at the export nox-poetry ran: main dependencies only. birdtally has none, so the constraints file was empty and pytest
was not pinned by the lock at all; it got 9.1.1 only because that was also the newest release. With nox-poetry, dependencies from
groups are not locked; the poetry sync session is. nox-poetry needs poetry export, that is the export plugin (see
Exporting Requirements & Bundling).
pre-commit
The Poetry repository provides four hooks, each running the Poetry command of the same name with the same arguments:
| Hook | Purpose |
|---|---|
|
|
|
|
|
|
|
|
repos:
- repo: https://github.com/python-poetry/poetry
rev: 2.5.1
hooks:
- id: poetry-check
args: ["--lock"]
- id: poetry-lock
Giving args: replaces the hook’s defaults, so repeat them all. The documentation’s poetry-export example still passes the
deprecated --dev; use --with dev (or --all-groups) instead. For a pyproject.toml in a subdirectory add ["-C",
"./subdirectory"]. After adding a dependency to pyproject.toml by hand without locking:
pre-commit run --all-files
poetry-check.............................................................Failed
- hook id: poetry-check
- exit code: 1
Error: pyproject.toml changed significantly since poetry.lock was last generated. Run `poetry lock` to fix the lock file.
poetry-lock..............................................................Failed
- hook id: poetry-lock
- files were modified by this hook
Resolving dependencies...
Writing lock file
The commit is stopped, and poetry.lock is updated in the working tree, ready to be added. With both files consistent the hooks
print Passed.
pre-commit autoupdate takes the newest tag on the default branch, but Poetry tags its releases on stable branches, so
autoupdate does not move to the latest Poetry. Update rev by hand, restrict autoupdate with --repo, or use the third-party
pre-commit-update hook.
Docker: build with Poetry, ship the environment
CI Caching & Containers shows the layer-caching two-step in a single-stage image. A multi-stage build adds two things: Poetry and its own dependencies stay out of the runtime image, and the project is installed from its wheel instead of in editable mode, so the environment can be copied on its own:
# syntax=docker/dockerfile:1
FROM python:3.14-slim AS builder
ENV POETRY_VERSION=2.5.1 \
POETRY_HOME=/opt/poetry \
POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1 \
PIP_NO_CACHE_DIR=1
RUN python -m venv "$POETRY_HOME" \
&& "$POETRY_HOME/bin/pip" install "poetry==$POETRY_VERSION"
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN --mount=type=cache,target=/root/.cache/pypoetry \
"$POETRY_HOME/bin/poetry" sync --only main --extras pretty --no-root --no-directory
COPY README.md LICENSE CHANGELOG.md ./
COPY src/ ./src
RUN "$POETRY_HOME/bin/poetry" build --format wheel \
&& .venv/bin/pip install --no-deps dist/*.whl
FROM python:3.14-slim AS runtime
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
USER nobody
ENTRYPOINT ["birdtally"]
docker build -t birdtally:poetry-ms .
docker images birdtally --format '{{.Tag}} {{.Size}}'
docker run --rm -v "$PWD/survey.csv:/data/survey.csv:ro" birdtally:poetry-ms /data/survey.csv
#14 [builder 8/8] RUN "/opt/poetry/bin/poetry" build --format wheel && .venv/bin/pip install --no-deps dist/*.whl
#14 0.777 Building birdtally (0.1.0)
#14 0.781 Building wheel
#14 0.783 - Building wheel
#14 0.790 - Built birdtally-0.1.0-py3-none-any.whl
#14 2.761 Processing ./dist/birdtally-0.1.0-py3-none-any.whl
#14 2.866 Installing collected packages: birdtally
#14 2.878 Successfully installed birdtally-0.1.0
#14 DONE 3.0s
#15 [runtime 2/2] COPY --from=builder /app/.venv /app/.venv
#15 DONE 0.2s
…
poetry-ms 203MB
pip 224MB
poetry 346MB
{
"lapwing": 7,
"redshank": 2
}
The multi-stage image (203 MB) is smaller than the single-stage Poetry image (346 MB) of the containers page and the pip image
(224 MB); the base image is 179 MB. The runtime stage holds birdtally, rich with its three dependencies and pip, and no
poetry command. Rules that make it work:
-
Use the same base image in both stages: the copied venv points at the base image’s interpreter by absolute path.
-
Keep
.venv,distand.gitout of the build context with a.dockerignore, or a local.venvis copied over the built one. -
--mount=type=cachekeeps Poetry’s download cache between builds without putting it in a layer; it needs BuildKit, the default builder. -
Run as a non-root user in the runtime stage.
The other route, exporting a hashed requirements.txt and installing it with pip --no-deps in an image without Poetry, is on
CI Caching & Containers.
The image was built through the machine’s TLS-inspecting proxy with --network host --build-arg HTTPS_PROXY=… and a base
image that trusts the proxy’s CA; neither belongs in a normal build.
Continuous integration
Poetry’s documentation has no GitHub Actions or CI-caching guide: its CI advice is the installer’s "CI recommendations" (pin the
version, install with pipx or into a dedicated venv) and the Docker FAQ entry above. A workflow that caches Poetry’s environments
with actions/setup-python is on
CI Caching & Containers; release
workflows are on
GitHub Actions & GitLab CI Release Workflows.
Source: Poetry: CI recommendations.