Poetry (Python)
A pyproject.toml-centric dependency manager, build backend, and virtual-environment manager for Python that locks one project at a time — with no native workspace concept, so monorepos are assembled from relative path dependencies, dependency groups, and third-party plugins.
| Field | Value |
|---|---|
| Language | Python (CLI written in Python; poetry-core is the PEP 517 build backend) |
| License | MIT |
| Repository | python-poetry/poetry |
| Documentation | python-poetry.org/docs |
| Category | Python Package Manager |
| Workspace model | None native. Per-project pyproject.toml + relative path deps; plugins for monorepo glue |
| First released | 0.1.0 — February 2018 |
| Latest release | 2.4.1 — May 9, 2026 |
Latest release:
2.4.1(2026-05-09). The2.xline (since2.0.0, 2025-01-05) pivoted Poetry onto the PEP 621[project]table as the primary metadata source, demoting the historical[tool.poetry]table. Per the CHANGELOG,2.2.0(2025-09-14) added PEP 735[dependency-groups]support and2.3.0(2026-01-18) addedpylock.tomlexport. None of these releases added aworkspaceprimitive — Poetry remains a single-project tool by design.
Overview
What it solves
Poetry consolidates what was historically a scattered toolchain — setup.py, requirements.txt, setup.cfg, MANIFEST.in, and Pipfile — into one declarative pyproject.toml, and pairs it with a lockfile (poetry.lock) and a per-project virtual environment. From the project README:
"Poetry helps you declare, manage and install dependencies of Python projects, ensuring you have the right stack everywhere."
The value proposition is deterministic, reproducible installs. The first poetry install resolves the dependency graph and, per the Basic usage docs, "writes all the packages and their exact versions that it downloaded to the poetry.lock file, locking the project to those specific versions." Thereafter every machine installs from the pinned lockfile so that "your CI server, production machines, other developers in your team, everything and everyone runs on the same dependencies."
Crucially, Poetry's unit of management is one project. There is exactly one pyproject.toml, one poetry.lock, and one virtual environment per project. This is the central tension for monorepos: Poetry has no concept of a multi-package workspace, a root manifest grouping members, a shared lockfile across packages, or a topological build loop. The Python equivalent of those features — if you want them — comes from uv (which does ship a native [tool.uv.workspace]), not from Poetry.
Design philosophy
Poetry's resolver is a PubGrub implementation, originally extracted as the standalone mixology library and now vendored in-tree at src/poetry/mixology/version_solver.py (source). PubGrub is a conflict-driven version-solving algorithm shared today by Poetry, uv, Cargo, Swift Package Manager, Hex, and Bundler; its hallmark is producing human-readable conflict explanations instead of an opaque "no solution".
The philosophy is strict, deterministic, application-grade dependency management for one project, prioritizing correctness of the resolved graph over flexibility of layout. That deliberate narrowness is exactly why monorepos feel bolted-on: Poetry never set out to be a build orchestrator. The official monorepo feature request (#6850) remains open with status/triage, and the recommended pattern there is still path dependencies combined with Poetry's group feature — not a workspace engine.
NOTE
Poetry occupies the same category as uv and hatch in this survey. Among the three, only uv offers a first-class workspace; Poetry and hatch both treat the monorepo as an assembly of independent projects. Compare with the JS package managers (pnpm, npm, yarn-berry), which all ship a workspace primitive, and with cargo's [workspace] table — the design Poetry conspicuously lacks.
How it works
The single-project triad: pyproject.toml, poetry.lock, venv
A Poetry project is a directory with one pyproject.toml. Since 2.0.0 the canonical form uses the PEP 621 [project] table for metadata and main dependencies, with a residual [tool.poetry] table for Poetry-specific knobs (dependency groups, package-mode, relative path deps, sources):
# pyproject.toml (Poetry 2.x, PEP 621 style)
[project]
name = "my-service"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.27",
"pydantic>=2.7",
]
[tool.poetry]
package-mode = true # default; false ⇒ application, not a distributable library
[build-system]
requires = ["poetry-core>=2.0"]
build-backend = "poetry.core.masonry.api"poetry install resolves this graph with PubGrub, writes pinned versions to poetry.lock, creates a virtual environment (by default under {cache-dir}/virtualenvs, or in-project as .venv when configured), and installs into it. poetry lock re-resolves without installing; poetry run <cmd> and poetry shell execute inside that venv.
Library vs. application: package-mode
Poetry decides whether to build a project from the package-mode key (pyproject docs):
"Whether Poetry operates in package mode (default) or not."
In the default package mode the project is a distributable library (poetry build produces a wheel/sdist). Setting package-mode = false marks it an application that is never built or published — only its dependencies are managed. In a monorepo, shared libs/* are package-mode true and deployable apps/* are typically package-mode = false. This is the closest analogue Poetry has to Cargo's root-package-vs-virtual-workspace distinction — but it is per-project metadata, not a topology declaration.
Building distributions: packages / include / from
A package-mode project declares what lands in the wheel via [tool.poetry] packages/include, where each entry names a package and, optionally, a from source directory (pyproject docs). Critically, includes must live under the project root — the constraint that breaks naive monorepo code-sharing (see Dependency Handling).
Workspace Declaration & Topology
Poetry has no workspace declaration. There is no root manifest, no members array, no glob discovery, and no virtual root. The CLI docs expose no workspace, foreach, --filter, --recursive, or -p/--package surface; from the Basic usage docs Poetry is "a per-project tool with one configuration file per project." A "monorepo" is therefore just a directory tree of independent Poetry projects, each with its own pyproject.toml, poetry.lock, and venv:
monorepo/
├── libs/
│ ├── core/
│ │ ├── pyproject.toml # package-mode = true (a library)
│ │ └── poetry.lock # its OWN lockfile
│ └── client/
│ ├── pyproject.toml
│ └── poetry.lock
└── apps/
└── api/
├── pyproject.toml # package-mode = false (an application)
└── poetry.lock # ALSO its own lockfileNothing ties these together at the Poetry level. There is no command to enumerate members, and each subtree resolves and locks in isolation. Topology, if it exists at all, is implied solely by the path dependencies one project declares on another (below) — there is no first-class graph the tool reasons about across the tree.
IMPORTANT
This is the defining gap versus cargo ([workspace] members = [...]), pnpm (pnpm-workspace.yaml globs), go-work (go.work use directives), and even Poetry's own ecosystem sibling uv ([tool.uv.workspace] members = ["libs/*"]). For the proposed dub [workspace] block, Poetry is the cautionary baseline — the cost of having no topology primitive.
Dependency Handling & Isolation
Per-project isolation via virtual environments
Isolation in Poetry is per-project venv, not hoisting or a content-addressed store. Each project gets its own virtual environment; there is no shared node_modules-style hoist and no global hard-linked store like pnpm's or uv's cache-backed installs. Two projects in the same tree that both depend on httpx install it twice, into two separate venvs, resolved independently against two separate lockfiles — the principal source of version drift across a Poetry monorepo.
Cross-package local references: relative path dependencies
The only mechanism for one local package to depend on a sibling is a path dependency, declared in [tool.poetry.dependencies] (relative paths are only allowed there, not in the PEP 621 [project] table). From the Dependency specification docs, verbatim:
"To depend on a library located in a local directory or file, you can use the
pathproperty:"toml[tool.poetry.dependencies] # directory my-package = { path = "../my-package/", develop = true } # file my-package = { path = "../my-package/dist/my-package-0.1.0.tar.gz" }
develop = true installs the sibling editable (a .pth/symlink into the source tree) so edits are picked up live — the functional equivalent of Yarn's workspace: protocol or Cargo's intra-workspace path resolution, but declared by hand, per-edge, with a brittle relative path.
WARNING
The PEP 621 [project] table cannot express relative paths. Per the docs, "In the project section, you can only use absolute paths" via PEP 508 URLs (my-package @ file:///absolute/path/...). Absolute paths are non-portable across machines, so monorepo cross-refs are effectively trapped in the legacy [tool.poetry.dependencies] table — a real friction point in the 2.x PEP 621 migration.
Dependency groups (organization within a project, not across projects)
Poetry's dependency groups ([tool.poetry.group.<name>.dependencies], plus PEP 735 [dependency-groups] since 2.2.0) organize one project's dependencies — test, docs, etc. — selectable with --with / --without / --only. The #6850 pattern co-opts groups to stage local path deps, but groups are explicitly intra-project: per the docs, they "must only contain dependencies you need in your development process." They are not a cross-package topology.
The build-time path-rewriting problem (and plugins)
path deps with develop = true work for local development but break on publish/Docker: a built wheel cannot carry a ../sibling reference, and Poetry forbids includes outside the project root. The poetry-multiproject-plugin exists precisely to paper over this, introducing a build-project command that, per its docs, addresses that "Poetry does not allow package includes outside of the project root." It works by:
- copying the project to a temp dir,
- collecting relative includes (
include = "foo/bar", from = "../../shared"), - copying the shared source in,
- rewriting
pyproject.tomlwith adjusted paths, - running
poetry build, then copyingdist/back.
A sibling poetry-monorepo-dependency-plugin instead rewrites relative path deps into pinned version deps at build/publish time. That these are third-party plugins, not core features, is itself the headline finding: Poetry's monorepo story is community-patched, not designed.
Task Orchestration & Scheduling
There is no task DAG, no scheduler, and no change detection. Poetry is not a task runner. poetry run <cmd> executes a single command in one project's venv (CLI docs); poetry build, poetry lock, and poetry install each operate on one project. There is:
- no
foreach-style broadcast across packages (contrastyarn-berry'syarn workspaces foreach), - no topological build ordering of local libraries before dependents,
- no input hashing / affected-detection /
--since <ref>slicing (contrastturborepo,nx,bazel), - no concurrency across members.
The only graph Poetry computes is the package dependency graph for resolution inside one project (PubGrub), surfaced read-only by poetry show --tree. To orchestrate work across a Poetry monorepo you reach for an external runner — make, just, task, or a JS-side orchestrator like turborepo/nx driving poetry invocations — exactly the "uncoordinated testing scripts" problem the dub proposal aims to eliminate.
NOTE
A common production setup wires Poetry as the per-package dependency/build tool underneath a polyglot orchestrator: the orchestrator owns the DAG, caching, and affected-detection; Poetry owns resolution and the venv. Poetry deliberately stays out of that layer.
Caching & Remote Execution
Poetry's caching is download/artifact caching for resolution, not build/test output caching, and there is no remote execution:
- Package cache. Downloaded wheels/sdists and HTTP metadata are cached under
{cache-dir}(poetry cache list/poetry cache clear), so re-resolving or re-installing avoids re-downloading. This is a fetch cache. - Lockfile as the reproducibility mechanism. Determinism comes from
poetry.lockpinning exact versions, not from a content-addressed build cache.2.0.0added locked markers and aninstaller.re-resolveoption (defaulttrue) so installs can skip re-resolution when the lock is current. - No task-output cache. Because there are no tasks (above), there is nothing analogous to
turborepo's.turbo,gradle's build cache, orbazel's action cache. - No REAPI / remote execution. Poetry has no remote-cache backend and no Remote Execution API client; there is no integration with
buildbuddy,buildbarn, ornativelink. - No content-addressed install store. Unlike
pnpm/uv, installs go into per-project venvs rather than a global hard-linked CAS, so disk usage scales with(projects × dependencies).
In short, Poetry caches what it downloads, reproducibly pins what it resolves, and caches nothing about what it builds or tests — because it builds and tests nothing across a workspace.
CLI / UX Ergonomics
The command boundary is always a single project — there are no target-selection flags because there are no multiple targets. From the CLI docs, the relevant filtering knobs are all intra-project group selection:
poetry install # resolve + install THIS project into its venv
poetry install --with docs # include an optional dependency group
poetry install --without test # exclude a group (--without beats --with)
poetry install --only main # only the named group(s)
poetry install --sync # prune anything not in the lock (was --remove-untracked)
poetry install --no-root # install deps but not the project itself
poetry add httpx@^0.27 # add a dependency to THIS project
poetry add ../libs/core --editable # add a sibling as an editable path dep
poetry lock # re-resolve THIS project's lock
poetry build # build THIS project's wheel/sdist
poetry run pytest # run a command in THIS project's venv
poetry env use 3.12 # select the interpreter for THIS project's venvThere is no --filter, no -p/--package, no :target colon syntax, no --recursive/--from, and no --since <ref>. To "run tests everywhere" you script a shell loop over subdirectories invoking poetry run pytest in each — which is precisely the manual coordination modern workspace tools eliminate. The --with / --without / --only triad is genuinely well-designed for one project's dimensions, but it does not generalize to across projects.
IMPORTANT
For the dub proposal, Poetry's CLI is the "before" picture: clean and ergonomic within a package, but with zero cross-package verbs. The proposed dub run -p <member>, dub test --workspace, and dub build --since <ref> have no Poetry analogue — they map instead onto yarn-berry, cargo, and turborepo.
Strengths
- Best-in-class single-project ergonomics. One
pyproject.toml, one lockfile, one venv;add/remove/install/lock/runare crisp and discoverable. - PubGrub resolver with readable conflict explanations — among the strongest error messages in Python packaging.
- Deterministic, reproducible installs via
poetry.lock, locked markers, and--syncpruning. - Standards-forward (
2.x): PEP 621[project], PEP 735 dependency groups, PEP 517 builds viapoetry-core,pylock.tomlexport. develop = trueeditable path deps make local sibling development immediate (live edits, no reinstall).package-mode = falsecleanly distinguishes deployable applications from publishable libraries.
Weaknesses
- No native workspace at all — no root manifest, no member discovery, no shared lockfile, no topology.
- Version drift across the tree: each project resolves and locks independently; the same dependency is pinned and installed N times.
- No task DAG, no orchestration, no affected-detection, no concurrency — cross-package work needs an external runner.
- No build/test output caching and no remote execution. Caching is download-only.
- Cross-package refs are brittle: relative
pathdeps live only in the legacy[tool.poetry.dependencies]table (PEP 621 forbids relative paths), andpathdeps don't survivebuild/publish without a plugin. - Monorepo support is community-plugin territory (
poetry-multiproject-plugin,poetry-monorepo-dependency-plugin), not a core, supported design. - The official monorepo issue (#6850) is unresolved, sitting in
status/triagewith no roadmap commitment.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
One project = one pyproject.toml + one poetry.lock + one venv | Simple, deterministic, reproducible per-project installs; small mental model | No cross-project topology; version drift; per-project venv duplication of shared deps |
PubGrub (mixology) resolver | Correct, conflict-driven solving with human-readable failure explanations | Slower than native-code solvers like uv's Rust PubGrub on large graphs |
Cross-package refs via relative path deps (develop = true) | Reuses ordinary dependency syntax; editable installs give live local development | Per-edge, hand-maintained relative paths; only valid in legacy [tool.poetry]; breaks on build/publish |
| Dependency groups instead of workspaces | Organizes a project's own dev/test/docs deps with clean --with/--without/--only | Intra-project only; co-opting them for monorepo staging is a workaround, not a topology |
package-mode flag (library vs. application) | Lets applications skip build/publish while still managing deps | It is per-project metadata, not a workspace root/virtual-root distinction |
| No task runner / no DAG / no output cache | Keeps Poetry focused on dependency management and packaging only | Forces an external orchestrator (make/just/turborepo) for any cross-package build/test |
| Monorepo glue left to third-party plugins | Keeps the core small; lets the ecosystem experiment (build-project, dep rewriting) | Fragmented, unsupported UX; behavior varies by plugin; the official issue stays unresolved |
PEP 621 [project] as primary metadata (2.x) | Standards alignment with the broader Python packaging ecosystem | Relative path deps regressed to a legacy table; absolute-only PEP 508 file URLs are non-portable |
Sources
- python-poetry/poetry — GitHub repository
- Poetry documentation home
- Poetry
README.md— "Poetry helps you declare, manage and install dependencies…" - Basic usage — venv,
poetry.lock, reproducibility - The
pyproject.tomlfile —package-mode,packages/include/from,[project]vs[tool.poetry] - Dependency specification —
pathdeps,develop = true, relative-vs-absolute - Managing dependencies — dependency groups, PEP 735
- Commands (CLI reference) —
install/add/lock/build/run, group flags - CHANGELOG —
2.0.0PEP 621,2.2.0PEP 735,2.3.0pylock.toml,2.4.1 - Monorepo support using groups and path dependencies — issue #6850 (open,
status/triage) src/poetry/mixology/version_solver.py— vendored PubGrub solversdispater/mixology— standalone PubGrub librarypoetry-multiproject-plugin—build-project, includes outside project rootpoetry-monorepo-dependency-plugin— rewrite path deps to pinned versions- Related deep-dives:
uv·hatch·pnpm·npm·yarn-berry·cargo·go-work·turborepo·nx·bazel· the D landscape