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
# 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
poetry new mylib
cd mylib
poetry add requests
poetry run python -c "import requests; print(requests.__version__)"
Output:
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 buildandpoetry publishmake this simple.
Common pitfalls
poetry addchangespyproject.tomlandpoetry.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
.venvyou created withpython -m venv. Usepoetry env infoto find where Poetry put the venv, or configurepoetry config virtualenvs.in-project trueto place it in.venvat your project root.
poetry shellactivates the managed venv in a new subshell. Exit withexit. Alternatively, prefix every command withpoetry runto avoid activating.
Richer example — full project workflow
# 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:
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
[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
| Command | Purpose |
|---|---|
poetry new <name> | Scaffold a new project |
poetry init | Interactively 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 install | Install all deps from lockfile |
poetry install --only main | Install only runtime deps (for prod Docker) |
poetry update | Upgrade all deps within constraints |
poetry show | List installed packages |
poetry run <cmd> | Run command in the venv |
poetry shell | Spawn a shell with venv activated |
poetry build | Build sdist and wheel |
poetry publish | Publish to PyPI (requires API token) |
poetry env info | Show venv location and Python version |
poetry config virtualenvs.in-project true | Place 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]
[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+)
[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.
| Syntax | Meaning | Example resolves to |
|---|---|---|
^1.2.3 | Allow non-breaking updates (same major) | >=1.2.3, <2.0.0 |
^0.2.3 | Same minor (0.x is special) | >=0.2.3, <0.3.0 |
^0.0.3 | Same patch (0.0.x is special) | >=0.0.3, <0.0.4 |
~1.2.3 | Allow patch updates only | >=1.2.3, <1.3.0 |
~1.2 | Same minor | >=1.2, <1.3 |
1.2.* | Wildcard | >=1.2.0, <1.3.0 |
>=1.2, <2.0 | Explicit range | as written |
1.2.3 | Exact pin | ==1.2.3 |
* | Any version | latest |
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.
[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:
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.
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:
Updating dependencies
Resolving dependencies... (1.8s)
Writing lock file
Snippet:
[[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.lockto version control — it's the source of truth forpoetry install. - Hash-checked: every install verifies SHA-256 against the lock.
- Single platform per lock: unlike
uv.lock,poetry.lockis not platform-universal. Developers on macOS may need a Linuxpoetry.lockfor CI — typically resolved by locking inside a Linux container. content-hashrecords the resolved state ofpyproject.toml. If you edit constraints by hand,poetry installwarns that the lock is out of date.
poetry add / remove / update / install
# 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.
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:
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:
# 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).
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:
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:
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.
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:
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
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:
============================= 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.
| Plugin | Purpose |
|---|---|
poetry-plugin-export | Export poetry.lock to requirements.txt (bundled since 1.5) |
poetry-dynamic-versioning | Compute version from git tags |
poetry-plugin-bundle | Bundle the app + venv into a tarball |
poetry-plugin-shell | Restore the poetry shell command (removed in 2.0) |
poetry-plugin-up | Upgrade dependencies past the existing constraints |
poetry-plugin-mono-repo-deps | Local-path resolution for monorepos |
poethepoet | Task runner that reads [tool.poe.tasks] |
poetry self add poetry-plugin-export
poetry self add poetry-dynamic-versioning[plugin]
poetry self show plugins
poetry self remove poetry-plugin-export
Output:
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:
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
| Feature | Poetry | uv | hatch | pdm | rye |
|---|---|---|---|---|---|
| Language | Python | Rust | Python | Python | Rust |
| Lockfile | poetry.lock | uv.lock (universal) | none (uses pip) | pdm.lock | requirements.lock |
| Speed | Slow | Fastest | Fast | Moderate | Fast |
| Python download | No | Yes | Yes | Yes | Yes |
| PEP 621 native | 2.0+ | Yes | Yes | Yes | Yes |
| Build backend | poetry-core | Any PEP 517 | hatchling | pdm-backend | Any |
| Workspaces | Yes | Yes | Yes (matrix) | Yes | Yes |
| Plugin ecosystem | Large | Small | Moderate | Moderate | Small |
| Drop-in pip surface | No | uv pip | No | pdm (limited) | No |
| Tool installer | No | uv tool (pipx-like) | No | No | rye tools |
| Best for | Existing Poetry teams | New projects, speed | Library authors | Standards 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 installdoes not install your project by default in--no-rootmode — 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-runningpoetry lock --no-updateto recompute hashes.
poetry installon a project with--only mainstill creates a venv — for true production Docker builds, also setvirtualenvs.create = falseand install into the system Python of a slim image.
Caret ranges and 0.x versions are surprising —
^0.2.3resolves 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 truebefore the firstpoetry install. It puts.venvnext topyproject.tomlso VS Code, PyCharm, and direnv find it automatically.
In CI, use
poetry install --sync --no-interaction --no-ansi --only mainfor production builds.--syncremoves packages that aren't in the lock;--no-ansikeeps logs clean.
If
poetry addis slow, setinstaller.parallel = true(default) and pre-populate the cache by runningpoetry installonce on a base CI image.
Real-world recipes
Recipe — start a publishable library
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:
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
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
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
# 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
# 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
poetry self add poethepoet
Output: (none — exits 0 on success)
[tool.poe.tasks]
test = "pytest -v"
lint = "ruff check ."
format = "ruff format ."
typecheck = "mypy src"
check = ["lint", "typecheck", "test"]
serve = "uvicorn main:app --reload"
poetry run poe check # runs lint, typecheck, test in order
poetry run poe serve
Output:
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
| Variable | Purpose |
|---|---|
POETRY_VIRTUALENVS_IN_PROJECT | true → place venv at ./.venv |
POETRY_VIRTUALENVS_CREATE | false → use ambient Python (Docker, CI) |
POETRY_VIRTUALENVS_PATH | Custom parent dir for venvs |
POETRY_NO_INTERACTION | Disable all prompts (1) |
POETRY_CACHE_DIR | Custom cache directory |
POETRY_HTTP_TIMEOUT | HTTP request timeout in seconds |
POETRY_INSTALLER_PARALLEL | Toggle parallel installs |
POETRY_INSTALLER_MAX_WORKERS | Concurrency cap |
POETRY_PYPI_TOKEN_PYPI | PyPI upload token |
POETRY_HTTP_BASIC_<REPO>_USERNAME | Basic auth username for <REPO> |
POETRY_HTTP_BASIC_<REPO>_PASSWORD | Basic auth password / token |
POETRY_REQUESTS_CA_BUNDLE | Custom CA bundle for self-signed corporate certs |