skip to content

poetry — Dependency Management & Packaging

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

17 min read 38 snippets deep dive

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.

[!NOTE] 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 build and poetry publish make this simple.

Common pitfalls#

[!WARNING] 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.

[!WARNING] 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.

[!TIP] 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#

# 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#

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]#

[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.

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
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.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#

# 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%

[!TIP] 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.

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]
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#

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)#

[!WARNING] 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.

[!WARNING] 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.

[!WARNING] 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.

[!WARNING] 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.

[!TIP] 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.

[!TIP] 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.

[!TIP] 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#

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#

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