Managing Dependency Intent with Poetry Groups and Locked Builds
Stop shipping dev tools to production. Learn how to use Poetry dependency groups and locked builds to separate runtime intent and ensure reproducible CI environments.
31 Oct 2025, 18:18 UTC

Production images that accidentally include pytest and mypy, or CI jobs that fail because a transitive dependency silently updated, are symptoms of the same problem: dependencies are not separated by intent and the environment is not locked. The useful takeaway is to use named dependency groups in Poetry to make intent explicit, pin the interpreter range in pyproject.toml, commit the poetry.lock file, and install from that lock in CI. This combination ensures that what ships to production is predictable and stripped of development tooling.
\n\nThe Problem: Mixed Intent and Non-Deterministic Solves
\nPoetry models project metadata in pyproject.toml under the [tool.poetry] section, including name, version, and a Python version constraint. This constraint is strictly enforced during the solve and install phases. While the poetry.lock file records exact resolved versions and transitive dependencies to ensure reproducibility, many teams treat all non-runtime packages as a single dev bucket.
\n\nImplementing Dependency Groups
\nDependency groups are available in Poetry 1.13 and later. Projects using older releases rely on [tool.poetry.dev-dependencies], which is now deprecated. Before implementing groups, verify your version in the project root:
\n\n# Run in project root\npoetry --version\n\nExpected check: The output must be 1.13 or higher. If the version is lower, groups will not be recognized, and you must either upgrade Poetry or continue using the deprecated dev-dependencies block.
\n\nWith a supported release, define your groups in pyproject.toml as follows:
\n\n[tool.poetry.group.test.dependencies]\npytest = '^8.0'\npytest-cov = '^6.0'\n\n[tool.poetry.group.docs.dependencies]\nmkdocs = '^1.5'
\n\nRuntime dependencies remain under [tool.poetry.dependencies]. You can then control the installation based on the environment:
\n- \n
- Production: poetry install --only main (installs only runtime dependencies). \n
- CI Testing: poetry install --group test (adds test tools to the environment). \n
Risk: Changing the install mode affects what is placed in the virtual environment. Be aware that editable installs and the --no-root flag change where packages are located, which can break assumptions about entry points or package discovery if your project scripts are not updated accordingly.
\n\nLocking the Build and Pinning the Interpreter
\nTo eliminate 'it works on my machine' errors, pin the interpreter range in pyproject.toml and commit the poetry.lock file to version control. This forces CI to use a deterministic solve rather than recalculating dependencies on every run.
\n\nExample configuration for a project requiring Python 3.11:
\n\n[tool.poetry]\nname = 'my-service'\nversion = '0.1.0'\ndescription = 'Example service'\npython = '^3.11'\n\n[tool.poetry.dependencies]\npython = '^3.11'\nrequests = '^2.31'
\n\nIn your CI pipeline, verify that the lock file is being respected without triggering a new resolution:
\n\n# Run in a clean virtualenv in CI\npoetry install --dry-run\n\nExpected check: The command should report that it will install from the lock file and should not trigger a new solve. You can further verify the installed versions using poetry show and check dependency isolation by comparing poetry show --tree before and after adding a new group.
\n\nTrade-offs and Limitations
\nLock file formats and solve behaviors can change between major Poetry releases. Upgrading the Poetry tool itself may require regenerating the poetry.lock file to avoid subtle resolution differences. Additionally, using multiple groups increases the configuration surface; teams must maintain discipline in naming groups and documenting which groups are required for specific CI stages.
\n\nLimitations: Groups are version-gated (1.13+), lock files are project-specific and must be committed to Git, and selective installs require explicit flags in your deployment scripts.
\n\nActionable Closing
\nTo stabilize your Python environments: verify you are on Poetry 1.13+, migrate non-runtime packages into named [tool.poetry.group] sections, pin your Python version in pyproject.toml, and commit your poetry.lock. Update your CI to use --only main for production builds and explicit --group flags for testing. This ensures clear intent and reproducible deployments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.