Poetry - Dependency Management and Packaging Done Right
Reading time: ~30 minutes | Level: Intermediate → Engineering
Before reading further, consider this scenario:
# Team member A installs dependencies on Monday:
# pip install -r requirements.txt → app starts, all tests pass
# Team member B installs the same file on Friday:
# pip install -r requirements.txt → ImportError on startup
# requirements.txt is identical. Python version is identical.
# The machine is identical. What happened?
What happened is that httpx - a transitive dependency of httpx-client which is listed in requirements.txt - released version 0.26.0 between Monday and Friday. That version dropped support for a parameter your code indirectly relied on. Your requirements.txt listed httpx-client>=2.0 but did not pin httpx itself, because httpx is not a direct dependency. pip resolved it differently on different days.
This is not a hypothetical edge case. It is the single most common cause of "works on my machine" failures in Python projects. Poetry's poetry.lock file solves it by pinning every package in the full dependency graph - direct and transitive - to an exact version and hash.
What You Will Learn
- What Poetry solves and why it exists alongside
pipandpip-tools - Installation with
pipxand why that matters poetry newvspoetry init: when to use eachpyproject.tomlstructure under Poetrypoetry.lock: what it contains, why it is deterministic, when to commit it- Every key command with engineering-level explanation
- Version constraints:
^,~, ranges - exactly what each allows - Dependency groups: separating dev, test, and docs dependencies
- Poetry virtualenv management
- CI integration and caching strategies
- Comparison with
pip-toolsand PDM
Prerequisites
- Lesson 02 (
pyproject.toml) - Poetry usespyproject.tomlas its configuration file - Lesson 02 (
pip) - understanding whatpipdoes helps explain what Poetry replaces - Lesson 01 (
venv) - Poetry manages virtualenvs; you need to understand what a virtualenv is
Part 1 - The Problem Poetry Solves
Python dependency management has four distinct concerns. Before Poetry, each was handled by a separate tool:
| Concern | Old toolchain | Poetry |
|---|---|---|
| Declaring dependencies | setup.py / setup.cfg / pyproject.toml | pyproject.toml [tool.poetry.dependencies] |
| Pinning exact versions | pip freeze > requirements.txt | poetry.lock |
| Managing virtualenvs | venv, virtualenv manually | built-in, automatic |
| Building and publishing | setuptools + twine separately | poetry build + poetry publish |
Poetry collapses all four into a single workflow. The key technical insight is that Poetry separates two things that requirements.txt conflates:
- What your package needs (abstract constraints:
requests>=2.28) - What is actually installed (concrete pinned versions:
requests==2.31.0,urllib3==2.1.0,certifi==2024.2.2, ...)
The pyproject.toml expresses the first. The poetry.lock expresses the second - for every package including all transitive dependencies.
Part 2 - Installation
Why pipx, Not pip
# Correct
pipx install poetry
# Common mistake - do not do this
pip install poetry
pipx installs command-line tools into isolated virtualenvs that are separate from any project environment. If you install Poetry with pip into a project's virtualenv and then activate a different virtualenv, Poetry disappears. If you install it globally with pip, it can conflict with your project's dependencies (Poetry itself has dependencies like requests, cachecontrol, crashtest - these should not pollute your projects).
pipx gives Poetry its own persistent, isolated environment while exposing the poetry command globally on your PATH.
# Install pipx first (if not already installed)
pip install --user pipx
pipx ensurepath
# Then install Poetry
pipx install poetry
# Verify
poetry --version
# Poetry (version 1.8.x)
Poetry releases frequently. Pin pipx install poetry==1.8.3 if you need reproducible Poetry itself across machines. Running pipx upgrade poetry updates it independently of any project.
Part 3 - poetry new vs poetry init
poetry new - Start a Fresh Project
poetry new my-project
Creates a complete project scaffold:
my-project/
pyproject.toml ← Poetry-managed config
README.md
my_project/
__init__.py
tests/
__init__.py
Use poetry new when starting a greenfield project that will be a Python package (installable, publishable).
# src layout (recommended for packages)
poetry new --src my-project
# Creates: src/my_project/__init__.py
poetry init - Add Poetry to an Existing Project
cd existing-project/
poetry init
Runs an interactive wizard that generates a pyproject.toml without touching your existing files. Use this when:
- Converting an existing project from
requirements.txtto Poetry - Adding Poetry to a project that already has source code
- Adding Poetry to a project that is not a package (a service or application)
Part 4 - pyproject.toml Under Poetry
[tool.poetry]
name = "my-project"
version = "0.1.0"
description = "A short description of the project"
authors = ["Your Name <[email protected]>"]
readme = "README.md"
license = "MIT"
homepage = "https://github.com/yourname/my-project"
repository = "https://github.com/yourname/my-project"
documentation = "https://my-project.readthedocs.io"
keywords = ["utility", "cli"]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
packages = [{include = "my_project", from = "src"}]
[tool.poetry.dependencies]
# Python version constraint
python = "^3.11"
# Direct dependencies
requests = "^2.28"
click = "^8.1"
pydantic = "^2.0"
[tool.poetry.group.dev.dependencies]
# Development-only: not installed in production
pytest = "^8.0"
pytest-cov = "^5.0"
ruff = "^0.4"
mypy = "^1.10"
pre-commit = "^3.7"
[tool.poetry.group.docs.dependencies]
mkdocs = "^1.6"
mkdocs-material = "^9.5"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
The [tool.poetry] section contains publishing metadata. The [tool.poetry.dependencies] section declares runtime dependencies with version constraints. The [tool.poetry.group.*] sections declare optional dependency groups installed only when requested.
Part 5 - poetry.lock: The Reproducibility Engine
When you run any poetry add or poetry install command, Poetry writes (or updates) poetry.lock. This file contains the exact resolved version of every package - including every transitive dependency - plus the SHA-256 content hash of each downloaded file.
A truncated example:
# poetry.lock (auto-generated - do not edit manually)
[[package]]
name = "certifi"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc105f..."},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859..."},
]
[[package]]
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.7"
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2..."},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a..."},
]
[package.dependencies]
certifi = ">=2017.4.17"
urllib3 = ">=1.21.1,<3"
Every poetry install on every machine reads this file and installs precisely these versions with hash verification. No network resolution needed. No version drift.
Commit poetry.lock for applications; do NOT commit it for libraries.
For an application (a web service, a CLI tool, an internal script), commit poetry.lock. Everyone on the team and every CI run installs the exact same dependency graph.
For a library (a package other people will install), do NOT commit poetry.lock. If you publish poetry.lock, users who pip install your-library ignore it - but developers contributing to your library may be confused by a lockfile that locks them into dependencies unnecessarily. Let users' package managers resolve what fits their environment. Document the minimum supported versions in pyproject.toml constraints instead.
Part 6 - The poetry add Workflow
The key difference from pip install: Poetry resolves the full dependency graph before touching pyproject.toml or poetry.lock. If the new package conflicts with an existing dependency, Poetry reports it immediately rather than installing a broken environment.
Part 7 - Key Commands
Adding and Removing Dependencies
# Add a runtime dependency (updates pyproject.toml + poetry.lock + installs)
poetry add requests
# Add with a specific version constraint
poetry add "requests>=2.28,<3.0"
poetry add "requests^2.28"
# Add to a dependency group (dev by default for --group dev)
poetry add --group dev pytest
poetry add --group docs mkdocs
# Add a git dependency (useful for unreleased packages)
poetry add git+https://github.com/org/repo.git
# Add an editable local path dependency
poetry add --editable ./libs/my-shared-lib
# Remove a dependency
poetry remove requests
poetry remove --group dev pytest
Installing
# Install all dependencies (reads poetry.lock if it exists)
poetry install
# Install only the main group - skip dev/docs groups (production install)
poetry install --only main
# Install specific groups
poetry install --with docs,test
# Install without a group
poetry install --without docs
# Do not install the root package itself (useful for services)
poetry install --no-root
# Sync: remove packages not in poetry.lock (strict reproducibility)
poetry install --sync
Updating
# Update ALL dependencies to latest versions allowed by constraints
# WARNING: this can break things - see the danger callout below
poetry update
# Update a single package
poetry update requests
# Update multiple specific packages
poetry update requests click pydantic
# Check what would be updated without doing it
poetry update --dry-run
poetry update without arguments updates ALL dependencies to the latest version allowed by your version constraints. In a large project with many dependencies, this can introduce multiple breaking changes simultaneously. If something breaks after poetry update, git diff poetry.lock shows exactly what changed - but isolating which package caused the breakage requires bisection. Prefer poetry update requests (targeted updates) over poetry update (global updates). Review poetry show --outdated first to understand what will change.
Running Code
# Run a command inside Poetry's managed virtualenv
poetry run python script.py
poetry run pytest
poetry run python -m mypackage
# Activate the virtualenv in the current shell
# (changes your shell's Python to Poetry's managed env)
poetry shell
# Now: python, pip, pytest all refer to Poetry's venv
# Exit the activated shell
exit
# or: deactivate (if poetry shell used the standard venv activation)
Inspecting the Environment
# Show all installed packages
poetry show
# Show dependency tree (why is a package installed?)
poetry show --tree
# Show only top-level dependencies
poetry show --top-level
# Show outdated packages
poetry show --outdated
# Show info about a specific package
poetry show requests
# Show info about Poetry's virtualenv
poetry env info
# List all virtualenvs Poetry manages for this project
poetry env list
# Use a specific Python version for this project's venv
poetry env use python3.12
poetry env use /usr/local/bin/python3.11
# Remove the virtualenv (useful for a clean reinstall)
poetry env remove python3.12
Validation and Locking
# Validate pyproject.toml for syntax and consistency
poetry check
# Verify poetry.lock is consistent with pyproject.toml
# (use in CI to ensure developers didn't forget to commit updated lockfile)
poetry lock --check
# Regenerate poetry.lock without installing
poetry lock
# Regenerate without updating existing pins (only adds new packages)
poetry lock --no-update
Building and Publishing
# Build both sdist (.tar.gz) and wheel (.whl) in dist/
poetry build
# Publish to PyPI (prompts for credentials or uses configured token)
poetry publish
# Publish to TestPyPI
poetry publish --repository testpypi
# Build + publish in one step
poetry publish --build
Part 8 - Version Constraints
Poetry uses a version constraint language. Getting constraints wrong is one of the most consequential mistakes in packaging.
The Caret ^ Constraint (Most Common)
The caret constraint allows changes that do not modify the left-most non-zero digit.
^1.2.3 → >=1.2.3, <2.0.0 (most common: allows minor + patch updates)
^1.2 → >=1.2.0, <2.0.0
^1 → >=1.0.0, <2.0.0
^0.2.3 → >=0.2.3, <0.3.0 (0.x.y: left-most non-zero is minor digit)
^0.0.3 → >=0.0.3, <0.0.4 (0.0.x: left-most non-zero is patch digit)
^0.0 → >=0.0.0, <0.1.0
^0 → >=0.0.0, <1.0.0
^1.2.3 allows 1.9.9 but NOT 2.0.0. This is NOT the same as >=1.2.3. If a library releases a major version (2.0.0) with breaking changes, your constraint correctly excludes it. But ^1.2.3 also allows 1.9.0 - which might add deprecations or behavioral changes. The caret is safe for libraries that follow SemVer correctly, but many libraries do not. Review release notes when running poetry update.
The Tilde ~ Constraint (Conservative)
The tilde constraint is more conservative than caret.
~1.2.3 → >=1.2.3, <1.3.0 (patch updates only)
~1.2 → >=1.2.0, <1.3.0 (patch updates only)
~1 → >=1.0.0, <2.0.0 (minor + patch updates - same as ^1)
Use ~ when you need to accept patch updates (bug fixes) but distrust minor version changes from a specific library.
Constraint Comparison
Explicit Range Constraints
# Explicit range: most readable, most explicit
python = ">=3.11,<3.14"
django = ">=4.2,<5.0"
# Exact pin (rare - only for very unstable libraries)
some-unstable-lib = "==1.4.3"
# Wildcard
requests = "2.*" # any 2.x version
Choosing the Right Constraint
| Scenario | Constraint | Reason |
|---|---|---|
| Mature library, follows SemVer | ^2.28 | Caret: safe for minor + patch updates |
| Library with frequent breaking changes | ~2.28.0 | Tilde: patch updates only |
| Your package works with multiple major versions | >=2.0,<4.0 | Explicit range: clear intent |
| Pinning Python version | >=3.11,<4.0 | Explicit: excludes Python 4.x when it arrives |
| Critical pinned dependency | ==1.4.3 | Exact: use sparingly, only when necessary |
Part 9 - Dependency Groups
Dependency groups separate concerns. The standard groups are main (default), dev, test, and docs.
[tool.poetry.dependencies]
# main group: installed by default for all users
python = "^3.11"
requests = "^2.31"
click = "^8.1"
[tool.poetry.group.dev.dependencies]
# Installed by default in development (along with main)
# Skipped with --only main in production
pytest = "^8.0"
pytest-cov = "^5.0"
ruff = "^0.4"
mypy = "^1.10"
pre-commit = "^3.7"
[tool.poetry.group.test.dependencies]
# Explicitly separate test deps from other dev tools
pytest = "^8.0"
pytest-cov = "^5.0"
pytest-mock = "^3.14"
factory-boy = "^3.3"
[tool.poetry.group.docs.dependencies]
mkdocs = "^1.6"
mkdocs-material = "^9.5"
mkdocstrings = {extras = ["python"], version = "^0.25"}
Groups map to installation commands:
# Development (installs main + dev groups, which is the default)
poetry install
# Production (installs main group only)
poetry install --only main
# CI test run (installs main + test, skips docs)
poetry install --with test --without docs
# Docs build (installs main + docs, skips dev)
poetry install --with docs --without dev
The dev group is special: Poetry installs it by default alongside main (when you run poetry install locally). All other custom groups (like test, docs) must be explicitly requested with --with group-name unless they are marked optional = false. Use this to your advantage: put tools you need in every development session in dev, and tools only needed in specific contexts (docs builds, integration test runs) in their own groups.
Part 10 - Virtual Environment Management
Poetry creates and manages a virtualenv for each project automatically.
# Where do Poetry virtualenvs live?
poetry env info
# Output includes:
# Virtualenv
# Python: 3.12.3
# Implementation: CPython
# Path: /Users/you/Library/Caches/pypoetry/virtualenvs/my-project-Hs3K8j-py3.12
# Executable: /Users/you/Library/Caches/.../bin/python
# On Linux: ~/.cache/pypoetry/virtualenvs/
# On macOS: ~/Library/Caches/pypoetry/virtualenvs/
# On Windows: C:\Users\you\AppData\Local\pypoetry\Cache\virtualenvs\
By default Poetry creates virtualenvs in ~/.cache/pypoetry/virtualenvs/ (platform-specific cache directory). Most IDEs (VS Code, PyCharm) expect the virtualenv to be inside the project directory (.venv/). Configure Poetry to create in-project virtualenvs:
poetry config virtualenvs.in-project true
This creates .venv/ inside your project root. Add .venv/ to .gitignore. VS Code and PyCharm automatically detect and use .venv/ without manual configuration.
This setting is global - it applies to all future projects. Existing projects need their virtualenv recreated: poetry env remove python3.12 && poetry install.
# Switch to a different Python version
poetry env use python3.12
# If the Python binary is not on PATH:
poetry env use /usr/local/bin/python3.12
# List all virtualenvs Poetry manages for this project
poetry env list
# Remove a virtualenv
poetry env remove python3.12
# Remove all virtualenvs for this project
poetry env remove --all
Part 11 - Poetry vs pip-tools vs PDM
All three tools solve the dependency pinning problem. Here is a direct comparison:
| Feature | pip-tools | Poetry | PDM |
|---|---|---|---|
| Lockfile | requirements.txt (pinned) | poetry.lock | pdm.lock |
| Transitive dep pinning | Yes | Yes | Yes |
| Hash verification | Yes | Yes | Yes |
| Virtualenv management | No (separate tool) | Yes (built-in) | Yes (built-in) |
| Publishing to PyPI | No (separate twine) | Yes (built-in) | Yes (built-in) |
| pyproject.toml native | Partial | Yes | Yes (PEP 517/621) |
| Dependency groups | Via multiple files | Yes (groups) | Yes (groups) |
| Version constraint syntax | PEP 508 | Poetry-specific | PEP 508 |
| Speed | Fast | Moderate | Fast (Rust resolver) |
| Learning curve | Low | Moderate | Moderate |
| Maturity | Mature (2012) | Mature (2018) | Newer (2020) |
| Best for | Simple projects, existing pip users | Full-featured packaging workflow | PEP-standards-first teams |
pip-tools is the lowest-friction entry point: it uses standard requirements.in files and generates pinned requirements.txt. Poetry is the most integrated: one tool for everything. PDM is the most standards-aligned: it follows PEP 517, PEP 518, PEP 621 strictly.
For new projects at engineering teams, Poetry or PDM are both strong choices. For teams already using pip, pip-tools is the path of least resistance.
Part 12 - CI Integration
GitLab CI
# .gitlab-ci.yml
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
POETRY_CACHE_DIR: "$CI_PROJECT_DIR/.cache/poetry"
POETRY_VIRTUALENVS_IN_PROJECT: "true" # .venv/ inside project for caching
default:
image: python:3.12-slim
before_script:
- pip install poetry --quiet
- poetry install --no-interaction --no-ansi
cache:
key:
files:
- poetry.lock
paths:
- .venv/
- .cache/pip/
- .cache/poetry/
test:
stage: test
script:
- poetry run pytest tests/ --tb=short -q --cov=src --cov-report=xml
coverage: '/TOTAL.+?(\d+%)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
lint:
stage: test
script:
- poetry run ruff check src/ tests/
- poetry run ruff format --check src/ tests/
- poetry run mypy src/
publish:
stage: deploy
script:
- poetry config pypi-token.pypi $PYPI_TOKEN
- poetry publish --build
rules:
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+/'
GitHub Actions
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Poetry
run: pipx install poetry
- name: Cache virtualenv
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install dependencies
run: poetry install --no-interaction --only main,dev
- name: Run tests
run: poetry run pytest tests/ -q
Key CI Flags
# --no-interaction: never prompt for input (required in CI)
# --no-ansi: disable color codes (cleaner CI logs)
# --only main: production install, skip dev deps
poetry install --no-interaction --no-ansi --only main
The cache key for the virtualenv should be the hash of poetry.lock. When poetry.lock changes (new dependencies added or updated), the cache misses and the environment is rebuilt. When poetry.lock is unchanged, the cached .venv/ is reused \text{---} reducing CI time from 2\text{--}3 minutes (full install) to under 10 seconds (cache hit).
Graded Practice
Level 1 \text{---} Predict the Behavior
Given this pyproject.toml snippet:
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.28.0"
click = "~8.1.0"
pydantic = ">=2.0,<3.0"
Answer for each constraint:
- Which versions of
requestsare allowed? - Which versions of
clickare allowed? - Which versions of
pydanticare allowed? - If
requests==3.0.0releases, willpoetry update requestsinstall it? - If
click==8.2.0releases, willpoetry update clickinstall it?
Show Answer
-
requests:^2.28.0→>=2.28.0, <3.0.0. Any version from2.28.0up to (but not including)3.0.0is allowed:2.28.1,2.29.0,2.31.0,2.99.9all qualify.3.0.0does not. -
click:~8.1.0→>=8.1.0, <8.2.0. Only patch updates within8.1.xare allowed:8.1.0,8.1.3,8.1.7.8.2.0does not qualify. -
pydantic:>=2.0,<3.0→ Any version2.0.0through2.x.ybut not3.0.0.2.0.0,2.5.0,2.99.0all qualify.3.0.0does not. -
No.
requests==3.0.0is outside the^2.28.0constraint (which is<3.0.0).poetry update requestswill install the latest version that satisfies<3.0.0\text{---} which might be2.32.0or whatever is newest \text{---} but will not touch3.0.0. -
No.
click==8.2.0is outside the~8.1.0constraint (which is<8.2.0).poetry update clickwill install the latest8.1.xpatch version. To allow8.2.0, you would need to change the constraint to^8.1.0or>=8.1.0,<9.0.0.
Level 2 \text{---} Debug This Setup
A developer on your team runs poetry install and gets:
SolverProblemError
Because my-project depends on both:
• requests (^2.31.0)
• old-client (^1.0.0) which requires requests (<2.20.0)
there is no solution that satisfies all constraints.
They try poetry install --no-interaction \text{---} same error. They try pip install -r requirements.txt from an old file \text{---} it "works" (no error). They come to you saying Poetry is broken.
- Is Poetry broken?
- Why did
pip install -r requirements.txtsucceed? - What are the three ways to resolve this?
- Which resolution is correct?
Show Answer
-
Poetry is not broken. Poetry is correctly detecting a real conflict: your project requires
requests>=2.31.0butold-clientrequiresrequests<2.20.0. These ranges do not overlap \text{---} no version ofrequestssatisfies both. -
pipsucceeded becausepipdoes not resolve the full dependency graph before installing.pip install -r requirements.txtinstalls packages one by one and does not check whether a later package's requirements conflict with an already-installed package. The result is a broken environment where two packages have incompatible dependencies, butpipreports no error. The bug manifests at runtime \text{---} usually as anImportErroror unexpected behavior. -
Three resolution paths:
- Option A: Remove
old-client. Find an actively maintained alternative that supportsrequests>=2.28. This is almost always the right answer ifold-clientis truly outdated. - Option B: Downgrade your direct
requestsconstraint to something both packages accept:requests = ">=1.0.0,<2.20.0". This is usually wrong \text{---} you are constraining your own app to use an old, potentially insecure version ofrequeststo accommodate a dependency. - Option C: Fork
old-clientand update it to support modernrequests. Expensive but correct if you cannot replace it.
- Option A: Remove
-
Option A is almost always correct. A package that requires
requests<2.20.0was last updated before January 2021. It is not maintained. Depending on it is a security and maintenance risk regardless of the Poetry conflict. The Poetry conflict is a feature: it forced you to notice a problem thatpipsilently hid.
Level 3 \text{---} Design Challenge
You are setting up Poetry for a team building a Django web application. The project has:
- Runtime dependencies: Django, DRF, celery, redis-py, boto3
- Development tools: pytest-django, factory-boy, coverage, ruff, mypy, django-stubs
- Documentation tooling: Sphinx, sphinx-autodoc-typehints
- A staging environment where docs tooling must not be installed
- A CI pipeline that runs: (1) linting, (2) tests with coverage, (3) type checking, (4) docs build
Design the complete pyproject.toml dependency section and the poetry install command for each environment. Include the CI caching strategy and explain why your group structure is correct.
Show Answer
[tool.poetry.dependencies]
python = "^3.12"
Django = "^5.0"
djangorestframework = "^3.15"
celery = {extras = ["redis"], version = "^5.3"}
redis = "^5.0"
boto3 = "^1.34"
[tool.poetry.group.dev.dependencies]
# Tools needed every time a developer works on the project
pytest = "^8.0"
pytest-django = "^4.8"
pytest-cov = "^5.0"
factory-boy = "^3.3"
ruff = "^0.4"
mypy = "^1.10"
django-stubs = {extras = ["compatible-mypy"], version = "^5.0"}
types-redis = "^4.6"
pre-commit = "^3.7"
[tool.poetry.group.docs.dependencies]
# Only for docs build \text{---} explicitly optional
Sphinx = "^7.3"
sphinx-autodoc-typehints = "^2.1"
sphinx-rtd-theme = "^2.0"
Install commands per environment:
# Developer workstation: main + dev (default)
poetry install
# Production/staging server: main only
poetry install --only main
# CI lint job: main + dev (ruff is in dev)
poetry install --only main,dev
# CI test job: main + dev (pytest is in dev)
poetry install --only main,dev
# CI type check job: main + dev (mypy + stubs are in dev)
poetry install --only main,dev
# CI docs build job: main + docs only
poetry install --only main,docs
CI caching strategy:
# .gitlab-ci.yml
variables:
POETRY_VIRTUALENVS_IN_PROJECT: "true"
cache:
key:
files:
- poetry.lock # cache invalidates only when lockfile changes
prefix: "$CI_JOB_NAME" # separate cache per job (lint vs test vs docs)
paths:
- .venv/
The prefix: "$CI_JOB_NAME" is important: the lint job installs main,dev; the docs job installs main,docs. If they shared a cache, the docs job might use a .venv/ from the lint job that is missing Sphinx - or vice versa. Separate caches per job name ensure each job gets the right virtualenv.
Why the group structure is correct:
-
There is no separate
testgroup - test tools (pytest,pytest-django,factory-boy) belong indev. Every developer runs tests locally. Separating them into atestgroup would require every developer to add--with teston every install, which is friction without benefit. -
docsis its own group because Sphinx and its plugins are large, slow to install, and not needed on developer workstations or production servers. Only the docs build job needs them. Keeping them separate makes every otherpoetry installfaster. -
pre-commitis indevbecause it needs to be available locally for developers to runpre-commit install. CI runspre-commitdifferently (via the pre-commit cache, not the project venv).
Key Takeaways
- Poetry solves four concerns in one tool: dependency resolution, lockfile management, virtualenv management, and publishing. Before Poetry, each required a separate tool.
- Install Poetry with
pipx, notpip.pipxisolates Poetry's own dependencies from your project, giving you a stable globalpoetrycommand regardless of which virtualenv is active. poetry.lockis the source of truth for what is installed. It pins every package - direct and transitive - to an exact version with SHA-256 hash verification. Everypoetry installon every machine installs precisely the same dependency graph.- Commit
poetry.lockfor applications; do not commit it for libraries. Applications need reproducibility across machines and CI runs. Libraries need to let users' package managers resolve the best compatible versions for their environment. - Use
^for most constraints (^1.2.3→>=1.2.3, <2.0.0). Use~when you want only patch updates (~1.2.3→>=1.2.3, <1.3.0). Use explicit ranges (>=1.2,<2.0) when you need maximum clarity. Never use unconstrained*in production. poetry updatewithout arguments is dangerous: it updates all dependencies at once, potentially introducing multiple breaking changes that are hard to isolate. Preferpoetry update requests(targeted) and always reviewgit diff poetry.lockafter any update.- Dependency groups (
[tool.poetry.group.dev],[tool.poetry.group.docs]) separate development tooling from runtime dependencies. Usepoetry install --only mainin production to avoid installing pytest, mypy, and Sphinx on production servers. poetry env infoandpoetry env use python3.12give you precise control over which Python version Poetry's virtualenv uses. Setvirtualenvs.in-project = trueglobally for IDE compatibility.- In CI: cache the
.venv/directory keyed onpoetry.lockhash. A lockfile cache hit drops install time from 2–3 minutes to under 10 seconds. Use--no-interaction --no-ansiin all CIpoetry installcalls. poetry lock --checkin CI verifies thatpoetry.lockis up to date withpyproject.toml. If a developer added a dependency topyproject.tomlbut forgot to runpoetry lock, this command catches it.
