cheat sheet

poetry

Manage Python project dependencies, virtual environments, and package publishing with Poetry. Covers pyproject.toml, lockfiles, groups, and publishing to PyPI.

poetry — Dependency Management & Packaging

What it is

Poetry manages Python project dependencies through pyproject.toml, creates and manages virtual environments automatically, and provides a workflow for building and publishing packages to PyPI. It replaced setup.py/requirements.txt/virtualenv combinations for many teams.

uv offers a faster alternative for most poetry workflows in 2026. Poetry is still widely used and is the right choice if you're already on it or your team prefers its DX.

Install

bash
# Official installer (recommended — does not pollute your project venv)
curl -sSL https://install.python-poetry.org | python3 -

# Or via pip (simpler, may cause version conflicts)
pip install poetry

Output: (none — exits 0 on success)

Quick example — new project

bash
poetry new mylib
cd mylib
poetry add requests
poetry run python -c "import requests; print(requests.__version__)"

Output:

text
Created package mylib at mylib/
Using version ^2.32.3 for requests
Updating dependencies
Resolving dependencies... (0.3s)

Writing lock file

Package operations: 4 installs, 0 updates, 0 removals
  • Installing certifi (2024.2.2)
  • Installing charset-normalizer (3.3.2)
  • Installing idna (3.7)
  • Installing requests (2.32.3)

2.32.3

When / why to use it

  • You want a single tool for environment management, dependency resolution, and publishing.
  • Your project needs strict lockfiles (for reproducible CI builds).
  • You're publishing a library to PyPI — poetry build and poetry publish make this simple.

Common pitfalls

poetry add changes pyproject.toml and poetry.lock — always commit both files. The lockfile ensures every developer and CI run gets the exact same package versions.

Poetry creates its own venv — it does not use the .venv you created with python -m venv. Use poetry env info to find where Poetry put the venv, or configure poetry config virtualenvs.in-project true to place it in .venv at your project root.

poetry shell activates the managed venv in a new subshell. Exit with exit. Alternatively, prefix every command with poetry run to avoid activating.

Richer example — full project workflow

bash
# Start a new project
poetry new my-api
cd my-api

# Add runtime and dev dependencies
poetry add "fastapi>=0.111" "uvicorn[standard]"
poetry add --group dev "pytest>=8" ruff mypy

# Install all deps (including dev) into the venv
poetry install

# Run tests
poetry run pytest

# Build sdist + wheel
poetry build

Output:

text
Package operations: 16 installs, 0 updates, 0 removals
  • Installing anyio (4.4.0)
  • Installing fastapi (0.111.1)
  • Installing uvicorn (0.30.1)
  ...

============================= test session starts ==============================
collected 0 items
============================== no tests ran in 0.05s ==============================

Building my-api (0.1.0)
  - Building sdist
  - Built my-api-0.1.0.tar.gz
  - Building wheel
  - Built my_api-0.1.0-py3-none-any.whl

pyproject.toml structure

toml
[tool.poetry]
name = "my-api"
version = "0.1.0"
description = "A sample API"
authors = ["Alice Dev <alice@example.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.12"
fastapi = ">=0.111"
uvicorn = {extras = ["standard"], version = ">=0.30"}

[tool.poetry.group.dev.dependencies]
pytest = ">=8"
ruff = "*"
mypy = "*"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Essential commands

CommandPurpose
poetry new <name>Scaffold a new project
poetry initInteractively create pyproject.toml in existing directory
poetry add <pkg>Add a dependency
poetry add --group dev <pkg>Add a dev dependency
poetry remove <pkg>Remove a dependency
poetry installInstall all deps from lockfile
poetry install --only mainInstall only runtime deps (for prod Docker)
poetry updateUpgrade all deps within constraints
poetry showList installed packages
poetry run <cmd>Run command in the venv
poetry shellSpawn a shell with venv activated
poetry buildBuild sdist and wheel
poetry publishPublish to PyPI (requires API token)
poetry env infoShow venv location and Python version
poetry config virtualenvs.in-project truePlace venv in .venv/

pyproject.toml schema for Poetry

Poetry stores everything in pyproject.toml under [tool.poetry] (its legacy layout) or directly in [project] (since Poetry 2.0, which adopted PEP 621). Most existing projects still use [tool.poetry] — both forms are supported.

Legacy layout — [tool.poetry]

toml
[tool.poetry]
name = "my-api"
version = "0.1.0"
description = "A sample API"
authors = ["Alice Dev <alice@example.com>"]
license = "MIT"
readme = "README.md"
homepage = "https://example.com"
repository = "https://github.com/alicedev/my-api"
documentation = "https://example.com/docs"
keywords = ["api", "fastapi", "sample"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Programming Language :: Python :: 3.12",
    "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
]
packages = [{ include = "my_api", from = "src" }]
include = ["CHANGELOG.md"]
exclude = ["tests/"]

[tool.poetry.dependencies]
python = "^3.12"
fastapi = ">=0.111"
uvicorn = { extras = ["standard"], version = ">=0.30" }
httpx = { version = ">=0.27", optional = true }

[tool.poetry.group.dev.dependencies]
pytest = ">=8"
ruff = "*"
mypy = "*"

[tool.poetry.group.docs.dependencies]
mkdocs = ">=1.6"
mkdocs-material = "*"

[tool.poetry.extras]
http2 = ["httpx"]

[tool.poetry.scripts]
my-api = "my_api.cli:main"

[tool.poetry.urls]
"Bug Tracker" = "https://github.com/alicedev/my-api/issues"

[build-system]
requires = ["poetry-core>=1.9"]
build-backend = "poetry.core.masonry.api"

PEP 621 layout (Poetry 2.0+)

toml
[project]
name = "my-api"
version = "0.1.0"
description = "A sample API"
authors = [{ name = "Alice Dev", email = "alice@example.com" }]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.111",
    "uvicorn[standard]>=0.30",
]

[project.optional-dependencies]
http2 = ["httpx>=0.27"]

[project.scripts]
my-api = "my_api.cli:main"

[tool.poetry]
packages = [{ include = "my_api", from = "src" }]

[tool.poetry.group.dev.dependencies]
pytest = ">=8"
ruff = "*"

[build-system]
requires = ["poetry-core>=2.0"]
build-backend = "poetry.core.masonry.api"

PEP 621 is the future-proof choice — it makes the project portable to other build backends (hatchling, setuptools) without rewriting metadata.

Version constraints

Poetry supports a richer set of constraint syntaxes than pip, including caret (^) and tilde (~) ranges familiar from npm. Understanding these is essential because poetry add chooses one for you.

SyntaxMeaningExample resolves to
^1.2.3Allow non-breaking updates (same major)>=1.2.3, <2.0.0
^0.2.3Same minor (0.x is special)>=0.2.3, <0.3.0
^0.0.3Same patch (0.0.x is special)>=0.0.3, <0.0.4
~1.2.3Allow patch updates only>=1.2.3, <1.3.0
~1.2Same minor>=1.2, <1.3
1.2.*Wildcard>=1.2.0, <1.3.0
>=1.2, <2.0Explicit rangeas written
1.2.3Exact pin==1.2.3
*Any versionlatest
bash
poetry add "fastapi@^0.111"       # caret — most common, what `poetry add` defaults to
poetry add "fastapi@~0.111"       # tilde — patch-level only
poetry add "fastapi@>=0.111,<0.120"  # explicit range
poetry add "fastapi@*"            # any version
poetry add "fastapi@latest"       # explicit latest
poetry add "fastapi==0.111.1"     # exact pin

Output: (none — exits 0 on success)

Dependency groups

Dependency groups partition pyproject.toml dependencies into named buckets — main, dev, docs, test, anything you want. Groups can be optional (skipped by default) and installed selectively.

toml
[tool.poetry.dependencies]
python = "^3.12"
fastapi = ">=0.111"

[tool.poetry.group.dev.dependencies]
pytest = ">=8"
ruff = "*"

[tool.poetry.group.docs]
optional = true

[tool.poetry.group.docs.dependencies]
mkdocs = ">=1.6"

[tool.poetry.group.benchmark]
optional = true

[tool.poetry.group.benchmark.dependencies]
pytest-benchmark = "*"

Install behavior:

bash
poetry install                          # main + dev (non-optional groups)
poetry install --without dev            # exclude dev
poetry install --with docs              # include optional docs
poetry install --with docs,benchmark    # multiple optional groups
poetry install --only main              # production install — only runtime deps
poetry install --only docs              # just one group
poetry install --sync                   # remove anything not in the lockfile

Output: (none — exits 0 on success)

The --only main install is the right choice for production Docker images — it leaves out pytest, ruff, mypy, mkdocs, and other dev-only weight.

Lockfile behavior — poetry.lock

poetry.lock records the exact resolved version of every direct and transitive dependency, plus SHA-256 hashes for each downloaded artifact. It is generated automatically by poetry add/update/lock and read by poetry install.

bash
poetry lock                       # regenerate poetry.lock without installing
poetry lock --no-update           # refresh hashes without changing versions
poetry lock --check               # verify lock matches pyproject.toml (CI)

Output:

text
Updating dependencies
Resolving dependencies... (1.8s)
Writing lock file

Snippet:

toml
[[package]]
name = "fastapi"
version = "0.111.1"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
    { file = "fastapi-0.111.1-py3-none-any.whl", hash = "sha256:..." },
    { file = "fastapi-0.111.1.tar.gz", hash = "sha256:..." },
]
[package.dependencies]
pydantic = ">=2"
starlette = ">=0.37,<0.38"

[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "abc123..."

Properties:

  • Always commit poetry.lock to version control — it's the source of truth for poetry install.
  • Hash-checked: every install verifies SHA-256 against the lock.
  • Single platform per lock: unlike uv.lock, poetry.lock is not platform-universal. Developers on macOS may need a Linux poetry.lock for CI — typically resolved by locking inside a Linux container.
  • content-hash records the resolved state of pyproject.toml. If you edit constraints by hand, poetry install warns that the lock is out of date.

poetry add / remove / update / install

bash
# add — installs and records in pyproject.toml
poetry add requests
poetry add "fastapi>=0.111"
poetry add "uvicorn[standard]"             # extras
poetry add --group dev pytest mypy ruff    # dev group
poetry add --group docs mkdocs             # named group
poetry add --optional httpx                # optional dependency
poetry add --editable ./libs/mylib         # editable install
poetry add git+https://github.com/owner/repo
poetry add git+https://github.com/owner/repo#main
poetry add git+https://github.com/owner/repo#v1.0.0
poetry add ./local-package                 # local path
poetry add --source private my-internal-lib

# remove — opposite of add
poetry remove requests
poetry remove --group dev pytest

# update — refresh dependencies within constraints
poetry update                              # update all
poetry update fastapi httpx                # update specific
poetry update --dry-run                    # preview without installing
poetry update --lock                       # only update poetry.lock, don't install

# install — install from poetry.lock
poetry install                             # main + dev groups
poetry install --no-root                   # skip installing the project itself
poetry install --sync                      # remove packages not in lock
poetry install --only main                 # production install
poetry install --with docs                 # include optional group

Output: (none — exits 0 on success)

Configuration

poetry config controls behavior globally (user config) or per-project (local config). Settings persist across runs.

bash
poetry config --list                                    # show all settings
poetry config virtualenvs.in-project true               # .venv in project root (recommended)
poetry config virtualenvs.create false                  # use external venv (CI/Docker)
poetry config virtualenvs.path ~/.poetry-venvs          # custom venv parent
poetry config virtualenvs.prefer-active-python true     # use the currently-active Python
poetry config installer.parallel true                   # parallel installs (default true)
poetry config installer.max-workers 10                  # cap concurrency
poetry config cache-dir ~/.cache/pypoetry                # cache location
poetry config repositories.private https://pypi.mycompany.com/simple/
poetry config http-basic.private alice "$PRIVATE_PYPI_TOKEN"
poetry config pypi-token.pypi "pypi-AgEIcHl..."         # PyPI upload token

# Local (per-project) — writes to poetry.toml
poetry config --local virtualenvs.in-project true

Output:

text
cache-dir = "/home/alice/.cache/pypoetry"
installer.max-workers = 10
installer.parallel = true
virtualenvs.create = false
virtualenvs.in-project = true
virtualenvs.path = "/home/alice/.poetry-venvs"
virtualenvs.prefer-active-python = true

Result of poetry config --local:

toml
# poetry.toml (committed to git)
[virtualenvs]
in-project = true

virtualenvs.in-project = true is the most impactful setting — it puts the venv at ./.venv instead of a hidden cache directory, so IDEs and tooling find it automatically.

Building and publishing

poetry build invokes the project's build backend (defaults to poetry-core) and produces a source distribution and wheel under dist/. poetry publish uploads to PyPI (or a configured private index).

bash
poetry build                                     # both sdist + wheel
poetry build -f sdist                            # source dist only
poetry build -f wheel                            # wheel only

poetry publish                                   # upload dist/* to PyPI
poetry publish --build                           # build then publish in one step
poetry publish --repository testpypi             # upload to TestPyPI
poetry publish --username __token__ --password "pypi-..."
poetry publish --skip-existing                   # don't fail if version already on PyPI

# Configure once, never type the token again
poetry config pypi-token.pypi "pypi-AgEIcHl..."
poetry config repositories.testpypi https://test.pypi.org/legacy/
poetry config pypi-token.testpypi "pypi-..."

Output of poetry build:

text
Building my-api (0.1.0)
  - Building sdist
  - Built my_api-0.1.0.tar.gz
  - Building wheel
  - Built my_api-0.1.0-py3-none-any.whl

Output of poetry publish:

text
Publishing my-api (0.1.0) to PyPI
 - Uploading my_api-0.1.0.tar.gz 100%
 - Uploading my_api-0.1.0-py3-none-any.whl 100%

Use Trusted Publishing on PyPI (OIDC) to avoid storing tokens. Configure it once in PyPI's UI and your GitHub Actions workflow can upload without pypi-token.

Virtual environment management

Poetry creates a venv per project automatically. Inspect, switch, or remove them with poetry env.

bash
poetry env info                       # show active venv path, Python version, system info
poetry env list                       # list venvs for this project
poetry env list --full-path           # show full paths
poetry env use 3.12                   # switch project to Python 3.12 (creates new venv)
poetry env use /usr/bin/python3.11    # use a specific interpreter
poetry env remove python3.10          # delete a venv
poetry env remove --all               # nuke all venvs for this project

Output of poetry env info:

text
Virtualenv
Python:         3.12.4
Implementation: CPython
Path:           /home/alice/.cache/pypoetry/virtualenvs/my-api-AbCdEfGh-py3.12
Executable:     /home/alice/.cache/pypoetry/virtualenvs/my-api-AbCdEfGh-py3.12/bin/python
Valid:          True

Base
Platform: linux
OS: posix
Python:   3.12.4
Path:     /usr
Executable: /usr/bin/python3.12

poetry shell was removed in Poetry 2.0 — use poetry env activate (prints the command to run) or just prefix everything with poetry run.

poetry run and poetry shell

bash
poetry run python script.py
poetry run pytest -v
poetry run uvicorn main:app --reload

# Spawn a subshell with the venv activated (Poetry < 2.0)
poetry shell

# Poetry 2.0+ — prints the activation command
eval "$(poetry env activate)"

Output:

text
============================= test session starts ==============================
collected 24 items
tests/test_app.py ........................                                [100%]
============================== 24 passed in 0.84s ==============================
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

poetry run is the recommended day-to-day pattern — no subshells, no stale activations, and it works the same in scripts as it does interactively.

Plugin ecosystem

Poetry has a stable plugin API since 1.2. Plugins extend the CLI with new commands or hook into existing ones. Install with poetry self add.

PluginPurpose
poetry-plugin-exportExport poetry.lock to requirements.txt (bundled since 1.5)
poetry-dynamic-versioningCompute version from git tags
poetry-plugin-bundleBundle the app + venv into a tarball
poetry-plugin-shellRestore the poetry shell command (removed in 2.0)
poetry-plugin-upUpgrade dependencies past the existing constraints
poetry-plugin-mono-repo-depsLocal-path resolution for monorepos
poethepoetTask runner that reads [tool.poe.tasks]
bash
poetry self add poetry-plugin-export
poetry self add poetry-dynamic-versioning[plugin]
poetry self show plugins
poetry self remove poetry-plugin-export

Output:

text
Using version ^1.8.0 for poetry-plugin-export
poetry-plugin-export (1.8.0)
poetry-dynamic-versioning (1.4.0)

poetry export is the most-used plugin in practice — it converts poetry.lock to a requirements.txt for tools that don't speak Poetry:

bash
poetry export -f requirements.txt --output requirements.txt --without-hashes
poetry export -f requirements.txt --only main --output prod-requirements.txt
poetry export -f constraints.txt --output constraints.txt

Output: (none — exits 0 on success)

Comparison — Poetry vs uv vs hatch vs pdm vs rye

FeaturePoetryuvhatchpdmrye
LanguagePythonRustPythonPythonRust
Lockfilepoetry.lockuv.lock (universal)none (uses pip)pdm.lockrequirements.lock
SpeedSlowFastestFastModerateFast
Python downloadNoYesYesYesYes
PEP 621 native2.0+YesYesYesYes
Build backendpoetry-coreAny PEP 517hatchlingpdm-backendAny
WorkspacesYesYesYes (matrix)YesYes
Plugin ecosystemLargeSmallModerateModerateSmall
Drop-in pip surfaceNouv pipNopdm (limited)No
Tool installerNouv tool (pipx-like)NoNorye tools
Best forExisting Poetry teamsNew projects, speedLibrary authorsStandards purists(Now subsumed by uv)

Recommendation in 2026: new projects should start with uv unless the team has strong Poetry preferences. Migrating from Poetry to uv is a one-day job for most projects (export requirements.txt, run uv init and uv add, copy [tool.*] config).

Common pitfalls (additional)

poetry install does not install your project by default in --no-root mode — and the default behavior installs your project as editable into the venv. This can be confusing if you don't expect your own package to be importable.

Lockfile drift on team merges — when two PRs both run poetry add, the resulting lockfile merge conflict is messy. Resolve by accepting one side and re-running poetry lock --no-update to recompute hashes.

poetry install on a project with --only main still creates a venv — for true production Docker builds, also set virtualenvs.create = false and install into the system Python of a slim image.

Caret ranges and 0.x versions are surprising^0.2.3 resolves to >=0.2.3, <0.3.0, not >=0.2.3, <1.0.0. Many libraries break this convention; pin tightly if a dependency is unstable.

Always poetry config --local virtualenvs.in-project true before the first poetry install. It puts .venv next to pyproject.toml so VS Code, PyCharm, and direnv find it automatically.

In CI, use poetry install --sync --no-interaction --no-ansi --only main for production builds. --sync removes packages that aren't in the lock; --no-ansi keeps logs clean.

If poetry add is slow, set installer.parallel = true (default) and pre-populate the cache by running poetry install once on a base CI image.

Real-world recipes

Recipe — start a publishable library

bash
poetry new --src mylib
cd mylib
poetry add --group dev pytest ruff mypy
poetry version 0.1.0
poetry build
poetry config pypi-token.pypi "pypi-AgEIcHl..."
poetry publish

Output:

text
Created package mylib in mylib
Building mylib (0.1.0)
  - Building sdist
  - Built mylib-0.1.0.tar.gz
  - Building wheel
  - Built mylib-0.1.0-py3-none-any.whl
Publishing mylib (0.1.0) to PyPI
  - Uploading mylib-0.1.0-py3-none-any.whl 100%
  - Uploading mylib-0.1.0.tar.gz 100%

Recipe — production-ready FastAPI Docker image

dockerfile
FROM python:3.12-slim AS builder

ENV POETRY_VERSION=1.8.3 \
    POETRY_HOME=/opt/poetry \
    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

FROM python:3.12-slim
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . /app
WORKDIR /app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

POETRY_VIRTUALENVS_CREATE=false installs into the system Python; --no-root --no-directory skips installing your project so you can cache the dependency layer separately.

Recipe — GitHub Actions for Poetry

yaml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - name: Install Poetry
        run: pipx install poetry==1.8.3
      - name: Cache venv
        uses: actions/cache@v4
        with:
          path: .venv
          key: venv-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
      - run: poetry config virtualenvs.in-project true
      - run: poetry install --sync
      - run: poetry run pytest
      - run: poetry run ruff check .
      - run: poetry run mypy src

Recipe — migrate from Poetry to uv

bash
# Export Poetry's dependencies as a requirements.txt
poetry export -f requirements.txt --without-hashes --output requirements.txt
poetry export -f requirements.txt --only dev --without-hashes --output requirements-dev.txt

# Initialize uv in the same project
uv init --no-readme
uv add -r requirements.txt
uv add --dev -r requirements-dev.txt
uv sync

# Clean up after verifying
rm requirements.txt requirements-dev.txt poetry.lock
# Edit pyproject.toml to remove [tool.poetry.*] sections

Output: (none — exits 0 on success)

Recipe — private PyPI

bash
# Configure a private index
poetry config repositories.private https://pypi.mycompany.com/simple/
poetry config http-basic.private alice "$PRIVATE_PYPI_TOKEN"

# Add a package from the private index
poetry add --source private my-internal-tool

# pyproject.toml now contains:
# [[tool.poetry.source]]
# name = "private"
# url = "https://pypi.mycompany.com/simple/"
# priority = "supplemental"

Output: (none — exits 0 on success)

Recipe — task runner with poe

bash
poetry self add poethepoet

Output: (none — exits 0 on success)

toml
[tool.poe.tasks]
test = "pytest -v"
lint = "ruff check ."
format = "ruff format ."
typecheck = "mypy src"
check = ["lint", "typecheck", "test"]
serve = "uvicorn main:app --reload"
bash
poetry run poe check        # runs lint, typecheck, test in order
poetry run poe serve

Output:

text
Poe => ruff check .
All checks passed!
Poe => mypy src
Success: no issues found in 12 source files
Poe => pytest -v
============================== 24 passed in 0.91s ==============================
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Environment variables

VariablePurpose
POETRY_VIRTUALENVS_IN_PROJECTtrue → place venv at ./.venv
POETRY_VIRTUALENVS_CREATEfalse → use ambient Python (Docker, CI)
POETRY_VIRTUALENVS_PATHCustom parent dir for venvs
POETRY_NO_INTERACTIONDisable all prompts (1)
POETRY_CACHE_DIRCustom cache directory
POETRY_HTTP_TIMEOUTHTTP request timeout in seconds
POETRY_INSTALLER_PARALLELToggle parallel installs
POETRY_INSTALLER_MAX_WORKERSConcurrency cap
POETRY_PYPI_TOKEN_PYPIPyPI upload token
POETRY_HTTP_BASIC_<REPO>_USERNAMEBasic auth username for <REPO>
POETRY_HTTP_BASIC_<REPO>_PASSWORDBasic auth password / token
POETRY_REQUESTS_CA_BUNDLECustom CA bundle for self-signed corporate certs