poetry
Poetry is a command line tool that owns four jobs most Python projects handle with four separate tools: declaring dependencies, resolving them into an exact lock file, creating and managing the virtualenv they get installed into, and building and publishing the package to an index. You describe the project once in pyproject.toml and then run poetry add requests, poetry install, poetry build, poetry publish. The piece that earns its keep is poetry.lock: Poetry solves the full dependency graph once, writes every resolved version with its file hashes, and every later poetry install reproduces exactly that set on any machine, which pip alone does not give you. It also creates a virtualenv per project automatically, so poetry run pytest and poetry add work without you remembering to activate anything. Since 2.0 the project metadata lives in the standard PEP 621 [project] table rather than Poetry's own [tool.poetry] format, though the older format still works and most tutorials you find online still use it.
Poetry remains the most complete single tool for a pure Python project: locking, environments, groups, building and publishing all work and the documentation is good. The honest caveat in 2026 is speed, because uv does most of the same work far faster, so Poetry is the safe incumbent rather than the obvious choice for a new project.
Use it if
- You want a lock file with hashes for an application and a reproducible poetry install on every developer machine and CI runner, rather than a hand-maintained requirements.txt
- You want dependency groups: dev, docs, lint and test dependencies declared separately and installed selectively with --with, --without and --only, so your production image does not carry pytest
- You publish libraries to PyPI or a private index and want declaring, versioning, building the wheel and uploading to be four commands from the same tool with no setup.py
- You are tired of managing virtualenvs by hand: Poetry creates one per project, keeps it in sync, and poetry run and poetry env activate mean you never install into the system interpreter by accident
- Speed matters to you: uv resolves and installs the same dependency set in a fraction of the time, because Poetry's solver is pure Python and downloads metadata over the network to resolve, and for a large graph the difference is minutes rather than seconds
- You have a wide Python constraint and heavy scientific dependencies: a python = ">=3.9,<4.0" range makes the solver consider every supported interpreter version, and combining that with torch or tensorflow, whose wheels differ per platform and CUDA build, produces resolution runs that take a very long time or fail in ways that are hard to read
- Your package needs a real build step: poetry-core builds pure Python wheels and offers no supported way to compile a C extension, Cython module or Rust extension, so a project with native code needs setuptools, hatchling with a plugin, maturin or scikit-build-core instead
- You are following older documentation: 2.0 moved metadata to the [project] table, removed the default and secondary source priorities, and pushed poetry shell and poetry export out of core into separate plugins, so a large amount of what you find written about Poetry no longer matches the tool you installed
- You only need a lock file for an app and do not package anything: pip-tools compiles requirements.in to a pinned requirements.txt with no virtualenv management, no build backend and no plugin system, and it is a much smaller thing to understand and debug
- Your CI credentials story is complicated: Poetry uses the system keyring by default, which on a headless runner produces confusing failures until you set PYTHON_KEYRING_BACKEND to the null backend or feed credentials through POETRY_HTTP_BASIC environment variables
Setup reality
Do not pip install poetry into the project it is going to manage; it has a large dependency tree of its own (cleo, dulwich, keyring, virtualenv, requests, pbs-installer and more) and putting it in the same environment guarantees a conflict eventually. Use the official install.python-poetry.org script, pipx, or your OS package manager so it lives in its own environment. Python 3.10 or newer for Poetry itself, which is separate from the Python your project targets. First-run friction is mostly configuration: virtualenvs are created under a cache directory by default rather than in the project, and poetry config virtualenvs.in-project true is the setting almost everyone eventually wants so editors can find the interpreter. On Linux CI, the keyring backend is the classic failure: Poetry tries to talk to a secret service that does not exist and hangs or errors on any authenticated index, fixed by exporting PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring. If you are coming from Poetry 1.x, the 2.0 upgrade removed poetry shell (install poetry-plugin-shell or use poetry env activate) and poetry export (install poetry-plugin-export), changed poetry lock so it no longer updates already-locked packages unless you pass --regenerate, and replaced poetry install --sync with poetry sync. Docker builds need the two-step pattern of copying only pyproject.toml and poetry.lock, running poetry install --no-root, then copying the source, or you invalidate the dependency layer on every code change.
Patterns
Create a new project or adopt an existing onestart-a-project
poetry new my-service --src # scaffold src/my_service, tests, pyproject
cd existing-project && poetry init # interactive, writes pyproject.toml only
poetry check # validate pyproject.toml
poetry check --lock # and that poetry.lock matches itpoetry init only writes pyproject.toml; it does not create the virtualenv or install anything until you run poetry install. Run poetry check --lock in CI: it catches the case where someone edited pyproject.toml by hand and forgot to relock, which otherwise shows up as a mysterious version difference in production.
Add runtime and group dependenciesadd-dependencies
poetry add "fastapi>=0.115,<0.120"
poetry add --group dev pytest pytest-cov ruff
poetry add --group docs --optional sphinx
poetry add "uvicorn[standard]"
poetry add "myinternal @ git+ssh://git@github.com/acme/myinternal.git@v2.1"
poetry add --editable ../shared-libpoetry add resolves, updates the lock file and installs in one step, which is why it is slower than editing pyproject.toml by hand and why it is the right thing to do anyway. Without an explicit constraint Poetry writes a caret range such as ^0.115, which permits minor upgrades and is a bad default for zero-versioned packages where minor bumps are breaking.
Install exactly what is in the lock fileinstall-and-sync
poetry install # everything, including dev groups
poetry install --only main --no-root # production image
poetry install --with docs --without dev
poetry sync --only main # also removes anything not lockedpoetry install is additive: packages you removed from pyproject.toml stay in the virtualenv until you run poetry sync, which is what replaced poetry install --sync in 2.0. --no-root skips installing your own project, which is what you want in a Docker layer built before the source is copied. If poetry.lock is missing or stale, install resolves from scratch and your build is no longer reproducible.
Relock without accidentally upgrading everythingupdate-the-lock-file
poetry lock # keep already-locked versions, add new ones
poetry lock --regenerate # throw away the lock and resolve from scratch
poetry update requests # upgrade one package within its constraint
poetry update # upgrade everything within constraintsThis changed in 2.0: poetry lock now preserves existing pins by default, and the old --no-update flag is gone. Use --regenerate when you have changed source priorities or want a clean resolution. poetry update rewrites the lock and installs; if you only want the lock changed, in CI for a dependency bot for instance, use poetry lock and let the install happen separately.
Control which interpreter and where the venv livesmanage-virtualenvs
poetry config virtualenvs.in-project true # .venv inside the project
poetry env use 3.12 # or a full path to python
poetry env info --path
poetry env list
poetry env remove --all
eval $(poetry env activate) # replaces poetry shell
poetry run pytest -qBy default the virtualenv lives in a cache directory keyed by a hash of the project path, which means editors do not find it and moving the project directory orphans it; in-project true fixes both. poetry env activate prints the activation command rather than running it, so it needs the eval wrapper. If you want the old subshell behaviour, install poetry-plugin-shell.
Write the pyproject.toml a library needsdeclare-package-metadata
[project]
name = "my-service"
version = "1.2.0"
requires-python = ">=3.10"
dependencies = ["httpx (>=0.28,<0.29)"]
[project.scripts]
my-service = "my_service.cli:main"
[tool.poetry]
packages = [{ include = "my_service", from = "src" }]
[tool.poetry.dependencies]
python = ">=3.10,<3.14" # upper bound for locking only
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"Since 2.0 the standard [project] table is the right place for metadata, and [tool.poetry] is left for Poetry-specific settings such as the packages include list. Keep requires-python open in [project] so consumers are not constrained, and put the narrower upper bound under [tool.poetry.dependencies] so your own resolution stays tractable. A src layout needs the packages entry; without it Poetry guesses from the project name and quietly builds an empty wheel.
Resolve against a private package indexprivate-index
poetry source add --priority=supplemental internal https://pypi.acme.io/simple/
poetry source add --priority=explicit torch-cu124 https://download.pytorch.org/whl/cu124
# credentials, in CI
export POETRY_HTTP_BASIC_INTERNAL_USERNAME=ci
export POETRY_HTTP_BASIC_INTERNAL_PASSWORD=$TOKEN
export PYTHON_KEYRING_BACKEND=keyring.backends.fail.KeyringThe priorities are primary, supplemental and explicit; the old default and secondary were removed in 2.0. supplemental is only consulted when a package is not on PyPI, and explicit is never searched unless a dependency names that source, which is the correct setting for a wheel index like PyTorch's. The environment variable name is the source name uppercased with dashes turned into underscores, and the keyring line is what stops headless runners hanging on an absent secret service.
Build a Docker image without reinstalling on every code changedocker-layer-caching
FROM python:3.12-slim
ENV POETRY_VERSION=2.4.1 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_NO_INTERACTION=1
RUN pip install --no-cache-dir "poetry==$POETRY_VERSION"
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN poetry install --only main --no-root --no-directory
COPY src/ ./src/
RUN poetry install --only mainCopying only the two dependency files first is what makes the expensive layer cacheable. --no-directory is needed alongside --no-root when you have path dependencies, otherwise Poetry tries to install a directory that has not been copied yet. Setting POETRY_VIRTUALENVS_CREATE=false installs into the image's site-packages, which is fine because the container is already the isolation boundary; pinning POETRY_VERSION keeps a Poetry release from changing your image without warning.
Work out where a version came frominspect-dependencies
poetry show --tree # full forward tree
poetry show --tree --why urllib3 # what requires urllib3
poetry show --outdated
poetry show --only main --top-level
poetry show --format json | jq '.[].name'poetry show --tree on a large project prints a wall of text; adding --why for a single package inverts it and tells you which packages pull that one in, which is what you need when a transitive pin is blocking an upgrade. --outdated compares locked versions against the index and is the honest answer to what needs bumping, since a constraint like ^1.2 can be years behind the current release without anything complaining. --format json cannot be combined with --tree.
Ship a release to PyPI or a private indexbuild-and-publish
poetry version minor # 1.2.0 -> 1.3.0, edits pyproject.toml
poetry build # sdist and wheel into dist/
poetry build --format wheel
poetry config pypi-token.pypi $PYPI_TOKEN
poetry publish
poetry publish --repository internal --buildpoetry version only edits pyproject.toml; it does not commit or tag, so wiring it into a release job means adding the git steps yourself or using poetry-dynamic-versioning to read the version from tags. publish uploads whatever is already in dist/, including stale artifacts from a previous version, so clear the directory or pass --build. Trusted publishing on GitHub Actions avoids handling a token at all.
Install a Python version through Poetrymanage-interpreters
poetry python list
poetry python install 3.13
poetry env use 3.13
poetry python remove 3.13Added in 2.1 and backed by python-build-standalone through the pbs-installer dependency, so it downloads a prebuilt interpreter rather than compiling one. It is still marked experimental and does not cover every platform, so on a machine that already has pyenv or uv managing interpreters, adding a third manager is asking for confusion about which python is on PATH.
Add commands that used to be built inextend-with-plugins
poetry self add poetry-plugin-export
poetry self add poetry-plugin-shell
poetry self add poetry-dynamic-versioning
poetry self show plugins
poetry export -f requirements.txt --without-hashes -o requirements.txtpoetry self add installs into Poetry's own environment, not your project's, which is why plugins survive across projects and why installing them with plain pip into the project venv does nothing. export left core in 2.0; if a downstream tool needs a requirements.txt, this plugin is how you keep producing one, and dropping --without-hashes is worth it when the consumer supports hash checking.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| uv | PyPI | You want the same lock file and virtualenv workflow but resolved and installed one to two orders of magnitude faster, and you can accept a much younger tool |
| hatch | PyPI | You are publishing a library, want a build backend that supports plugins and custom build hooks, and care about test matrices across Python versions |
| pdm | PyPI | You want PEP 582 style local packages or a standards-first tool that has tracked PEP 621 and PEP 735 from the start |
| pip-tools | PyPI | You only need a pinned requirements.txt compiled from a requirements.in and want no virtualenv management, no build backend and no new file format |