Poetry Installs Are Additive by Default — When --sync Is the Right Call
Poetry's default install is additive, so stale packages linger in your virtualenv. Here's how groups, --with, and --sync change that — and the trade-offs.
03 Nov 2025, 22:10 UTC

The problem: a virtualenv that no longer matches the lock file
You delete a dependency from pyproject.toml, re-lock, and run poetry install. The replacement package appears. The package you removed is still importable. Nothing errors, so the drift stays hidden until a clean CI cache or a new colleague's machine behaves differently.
Useful takeaway: Poetry's default install is additive. If the environment must match poetry.lock exactly, you have to ask for that with --sync. Two terms used throughout: the lock file (poetry.lock) pins exact versions and hashes, and a virtualenv is the isolated Python environment Poetry installs into.
How Poetry decides what lands in the environment
Poetry reads dependency groups from pyproject.toml. The implicit main group holds runtime dependencies. Any other group under [tool.poetry.group.<name>] is installed only when selected — unless it is marked optional.
In current Poetry (1.8 and 2.x), poetry install installs the main group plus every non-optional group. A group with optional = true is skipped unless you pass --with <group>. Confirm the exact defaults for your version with poetry install --help.
Selection flags compose: --with adds groups, --without excludes them, and --only restricts the run to the listed groups. That composition is what lets one pyproject.toml serve app, test, and docs environments.
Worked example: a lint group that CI can sync
Define an optional group so it stays out of the default install:
[tool.poetry.group.lint]
optional = true
[tool.poetry.group.lint.dependencies]
ruff = "^0.6.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
Run everything from the repository root. No elevated permissions are needed; Poetry operates on a user-local virtualenv. If you are unsure which interpreter is active, check poetry env info --path.
Preview before touching anything:
poetry install --with lint --sync --dry-run
Expected check: the output lists packages to install and, critically, packages to remove. If the removal list contains something you still need, stop and adjust the group selection.
Apply it:
poetry install --with lint --sync
Verify the result (Poetry-created environments normally include pip):
poetry run pip list | grep -i ruff
poetry run python -c "import sys; print(sys.executable)"
The first command should show ruff; the second confirms you are inspecting the Poetry-managed environment rather than the system Python.
The trade-off, the limits, and the rollback
--sync makes the environment declarative, which is what you want in CI and for reproducible builds. The cost is that Poetry must compare the lock file against everything already installed, so the command is slower than a plain additive install, and it will delete packages the selected groups no longer require.
That deletion is the sharp edge. Suppose you manually pip installed a debugging tool into the same virtualenv. Plain poetry install --sync leaves untracked packages alone; recent versions add --remove-untracked to delete those too. Treat that as a separate, more aggressive decision and confirm it exists in your version with poetry install --help.
Limitations worth stating plainly: --sync only manages the active Poetry virtualenv. It does not clean other environments, system site-packages, or containers. It also assumes the lock file is current — if pyproject.toml changed without a re-lock, run poetry lock first, or check consistency with poetry check --lock (verify the flag name in your version).
Rolling back a sync
Because --sync changes state, plan the undo. Snapshot first: poetry run pip freeze > requirements.before.txt. If a removal was wrong, reinstall from that file with poetry run pip install -r requirements.before.txt, or re-run poetry install with the group set that previously included the package. There is no automatic undo, so the snapshot is the safety net.
A decision you can apply today
Use the default additive poetry install for fast local iteration, where a few extra packages cost nothing. Use --sync wherever the environment must equal the lock file: CI jobs, release pipelines, and long-lived developer environments. Keep rarely used tooling in optional groups so the default install stays lean, and reach for it with --with only when needed.
Start conservatively: add --dry-run to your existing install command this week and read the removal list. That single preview tells you how much drift has already accumulated.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.