Skip to main content
โšก Calmops

Poetry: Modern Python Packaging and Dependency Management

Table of Contents

Introduction

Python’s packaging ecosystem has a reputation for being fragmented and confusing. Developers often juggle multiple tools: pip for installing packages, setuptools for building distributions, requirements.txt for managing dependencies, and venv for isolated environments. Each tool has its own quirks, and they don’t always work seamlessly together.

Enter Poetry: a modern, unified solution that handles dependency management, virtual environment creation, and package building in one cohesive tool. Poetry brings the elegance and simplicity of dependency management from other languages (like Rust’s Cargo or Node.js’s npm) to the Python ecosystem.

In this guide, we’ll explore what Poetry is, why it matters, how it compares to traditional tools, and how to use it effectively in your projects.


What Is Poetry?

Poetry is a tool for dependency management and packaging in Python. It simplifies the entire workflow of creating, managing, and distributing Python projects by providing:

  1. Dependency management - Declare and resolve project dependencies
  2. Virtual environment management - Automatically create and manage isolated environments
  3. Package building - Build distributions (wheels and source distributions)
  4. Publishing - Publish packages to PyPI or private repositories
  5. Lock files - Ensure reproducible builds with deterministic dependency resolution

Poetry uses a single configuration file, pyproject.toml, to define your project’s metadata, dependencies, and build configuration. This replaces the need for multiple files like setup.py, setup.cfg, requirements.txt, and MANIFEST.in.

Key Characteristics

  • Declarative: Define what you want, not how to get it
  • Deterministic: Lock files ensure reproducible environments
  • Intuitive: Simple commands for common tasks
  • Modern: Follows Python packaging standards (PEP 517, PEP 518, PEP 440)
  • Integrated: Handles environments, dependencies, and packaging in one tool

The Problem Poetry Solves

Traditional Python Packaging Challenges

1. Multiple Tools Required

Traditional Python development requires juggling several tools:

# Create virtual environment
python -m venv venv
source venv/bin/activate

# Install packages
pip install flask requests

# Freeze dependencies
pip freeze > requirements.txt

# Build distribution
python setup.py sdist bdist_wheel

# Publish to PyPI
twine upload dist/*

Each tool has different syntax, different conventions, and different failure modes.

2. Dependency Resolution Issues

pip doesn’t resolve dependencies intelligently. If you have conflicting requirements, pip may install incompatible versions without warning:

# requirements.txt
package-a==1.0  # requires package-c>=2.0
package-b==1.0  # requires package-c<2.0

# pip install -r requirements.txt
# Installs incompatible versions without clear error

3. Reproducibility Problems

pip freeze captures all transitive dependencies, creating bloated requirements files that are hard to maintain:

# requirements.txt (pip freeze output)
certifi==2024.2.2
charset-normalizer==3.3.2
click==8.1.7
flask==3.0.0
idna==3.6
itsdangerous==2.1.2
jinja2==3.1.2
markupsafe==2.1.3
requests==2.31.0
urllib3==2.1.0
werkzeug==3.0.1
# ... 50+ more packages

You can’t tell which packages you explicitly installed versus which are dependencies of dependencies.

4. Environment Inconsistency

Different developers may have different environments, leading to “works on my machine” problems:

# Developer A
pip install flask==3.0.0

# Developer B
pip install flask  # Installs 3.1.0

# Different environments, different behavior

5. Complex Setup.py Files

Building and distributing packages requires complex setup.py files:

# setup.py (traditional approach)
from setuptools import setup, find_packages

setup(
    name="my-package",
    version="1.0.0",
    description="My package",
    author="Your Name",
    author_email="[email protected]",
    packages=find_packages(),
    install_requires=[
        "flask>=3.0.0",
        "requests>=2.31.0",
    ],
    extras_require={
        "dev": ["pytest", "black", "flake8"],
    },
    entry_points={
        "console_scripts": [
            "my-command=my_package.cli:main",
        ],
    },
)

How Poetry Solves These Problems

Poetry provides a unified interface that handles all these concerns:

# Create project with Poetry
poetry new my-project

# Add dependencies
poetry add flask requests

# Add development dependencies
poetry add --group dev pytest black flake8

# Install all dependencies
poetry install

# Build and publish
poetry build
poetry publish

All configuration lives in a single pyproject.toml file, and Poetry automatically manages virtual environments and lock files.


Poetry vs. Traditional Tools

Poetry vs. pip + requirements.txt

Aspect pip + requirements.txt Poetry
Dependency Declaration requirements.txt (flat list) pyproject.toml (structured)
Transitive Dependencies All listed (bloated) Separated (direct vs. transitive)
Dependency Resolution Basic (can miss conflicts) Advanced (detects conflicts)
Lock File Manual (pip freeze) Automatic (poetry.lock)
Environment Management Manual (venv) Automatic
Package Building Requires setup.py Built-in
Publishing Requires twine Built-in
Development Dependencies Separate requirements-dev.txt Integrated (groups)

Example: Dependency Declaration

With pip:

# requirements.txt
flask==3.0.0
requests==2.31.0
pytest==7.4.3
black==23.12.0

With Poetry:

# pyproject.toml
[tool.poetry.dependencies]
python = "^3.9"
flask = "^3.0.0"
requests = "^2.31.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
black = "^23.12.0"

Poetry clearly separates production and development dependencies, and uses semantic versioning constraints.

Poetry vs. Pipenv

Pipenv was an earlier attempt to unify Python packaging. While similar to Poetry, Poetry has several advantages:

Aspect Pipenv Poetry
Configuration File Pipfile pyproject.toml (standard)
Performance Slower dependency resolution Faster
Lock File Format Pipfile.lock (JSON) poetry.lock (TOML)
Community Adoption Declining Growing
Maintenance Less active Very active
PEP Compliance Partial Full (PEP 517, 518, 440)

Poetry is generally considered the modern successor to Pipenv.

Poetry vs. pip-tools

pip-tools is a lightweight alternative that generates lock files from requirements files:

Aspect pip-tools Poetry
Scope Dependency resolution only Full packaging solution
Configuration requirements.in pyproject.toml
Environment Management Manual Automatic
Package Building Not included Built-in
Publishing Not included Built-in
Learning Curve Shallow Moderate

Use pip-tools if you want minimal tooling. Use Poetry if you want a complete solution.


Understanding pyproject.toml

The pyproject.toml file is the heart of a Poetry project. It’s a standardized configuration file (defined in PEP 518) that replaces setup.py, setup.cfg, and requirements.txt.

Basic Structure

[tool.poetry]
name = "my-project"
version = "0.1.0"
description = "A brief description of my project"
authors = ["Your Name <[email protected]>"]
readme = "README.md"
license = "MIT"
homepage = "https://github.com/yourusername/my-project"
repository = "https://github.com/yourusername/my-project"
keywords = ["python", "packaging"]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
]

[tool.poetry.dependencies]
python = "^3.9"
flask = "^3.0.0"
requests = "^2.31.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
black = "^23.12.0"
flake8 = "^6.1.0"

[tool.poetry.scripts]
my-command = "my_project.cli:main"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Key Sections

[tool.poetry]: Project metadata

  • name: Package name
  • version: Current version
  • description: Short description
  • authors: List of authors
  • readme: Path to README file
  • license: License type
  • homepage, repository: Project URLs
  • keywords: Search keywords
  • classifiers: PyPI classifiers

[tool.poetry.dependencies]: Production dependencies

  • python: Python version constraint
  • Package names with version constraints

[tool.poetry.group.dev.dependencies]: Development dependencies

  • Testing, linting, formatting tools
  • Only installed when running poetry install (not when users install your package)

[tool.poetry.scripts]: Command-line entry points

  • Define CLI commands that users can run after installing your package

[build-system]: Build configuration

  • Specifies Poetry as the build backend (PEP 517)

Version Constraints

Poetry uses semantic versioning constraints:

# Exact version
flask = "3.0.0"

# Caret (^): Compatible with version
flask = "^3.0.0"  # >=3.0.0, <4.0.0

# Tilde (~): Approximately version
flask = "~3.0.0"  # >=3.0.0, <3.1.0

# Comparison operators
flask = ">=3.0.0"
flask = ">3.0.0,<4.0.0"

# Wildcard
flask = "3.0.*"  # >=3.0.0, <3.1.0

# Any version
flask = "*"

The caret (^) is the most common choice for dependencies, as it allows patch and minor updates while preventing major version changes.


Getting Started with Poetry

Installation

On macOS/Linux:

curl -sSL https://install.python-poetry.org | python3 -

On Windows (PowerShell):

(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -

Using Homebrew (macOS):

brew install poetry

Using pip:

pip install poetry

Verify installation:

poetry --version

Creating a New Project

poetry new my-project
cd my-project

This creates a project structure:

my-project/
โ”œโ”€โ”€ pyproject.toml
โ”œโ”€โ”€ README.md
โ”œโ”€โ”€ my_project/
โ”‚   โ””โ”€โ”€ __init__.py
โ””โ”€โ”€ tests/
    โ””โ”€โ”€ __init__.py

Initializing Poetry in an Existing Project

If you have an existing project, initialize Poetry:

cd my-existing-project
poetry init

Poetry will ask questions about your project and create a pyproject.toml file.

Adding Dependencies

Add a production dependency:

poetry add flask

This:

  1. Adds flask to pyproject.toml
  2. Resolves dependencies
  3. Creates/updates poetry.lock
  4. Installs the package in the virtual environment

Add a specific version:

poetry add flask==3.0.0

Add a development dependency:

poetry add --group dev pytest black flake8

Add multiple dependencies:

poetry add flask requests sqlalchemy
poetry add --group dev pytest pytest-cov black

Installing Dependencies

Install all dependencies (production and development):

poetry install

Install only production dependencies:

poetry install --no-dev

This is useful for deployment environments.

Updating Dependencies

Update all dependencies to their latest compatible versions:

poetry update

Update a specific package:

poetry update flask

Removing Dependencies

Remove a package:

poetry remove flask

Remove a development dependency:

poetry remove --group dev pytest

Running Commands in the Virtual Environment

Poetry automatically creates and manages a virtual environment. Run commands in it:

# Run Python
poetry run python script.py

# Run tests
poetry run pytest

# Run a CLI tool
poetry run black .

# Run your project's entry point
poetry run my-command

Or activate the virtual environment:

poetry shell
python script.py
pytest
black .
exit  # Exit the virtual environment

Understanding poetry.lock

The poetry.lock file is crucial for reproducibility. It’s automatically generated and updated by Poetry.

What It Contains

[[package]]
name = "flask"
version = "3.0.0"
description = "A simple framework for building web applications."
category = "main"
optional = false
python-versions = ">=3.8"

[package.dependencies]
click = ">=8.1.3"
itsdangerous = ">=2.1.2"
jinja2 = ">=3.1.2"
werkzeug = ">=3.0.0"

[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"

# ... more packages

Why It Matters

  1. Reproducibility: Lock files ensure everyone uses the exact same versions
  2. Deterministic Builds: Same dependencies every time, everywhere
  3. Conflict Detection: Records resolved dependency conflicts
  4. Audit Trail: Shows what was installed and when

Committing to Version Control

Always commit poetry.lock to version control:

# .gitignore
# Don't commit the virtual environment
.venv/
venv/

# Do commit the lock file
poetry.lock  # Commit this!

This ensures all developers and CI/CD systems use identical dependencies.

Updating the Lock File

Poetry automatically updates poetry.lock when you:

  • Add a dependency: poetry add flask
  • Update dependencies: poetry update
  • Change version constraints in pyproject.toml

To update the lock file without installing:

poetry lock

Dependency Resolution and Conflict Handling

Poetry’s dependency resolver is one of its key advantages. It intelligently resolves complex dependency trees and detects conflicts.

How It Works

When you add a dependency, Poetry:

  1. Fetches metadata from PyPI
  2. Analyzes requirements of the package and its dependencies
  3. Builds a dependency tree recursively
  4. Detects conflicts where requirements are incompatible
  5. Finds a solution that satisfies all constraints
  6. Locks the solution in poetry.lock

Example: Conflict Resolution

Suppose you have:

[tool.poetry.dependencies]
package-a = "^1.0"  # requires package-c >= 2.0
package-b = "^1.0"  # requires package-c < 2.0

With pip:

pip install package-a package-b
# Installs incompatible versions without clear error

With Poetry:

poetry add package-a package-b
# Error: Incompatible requirements detected
# package-a requires package-c >= 2.0
# package-b requires package-c < 2.0

Poetry clearly identifies the conflict and prevents installation.

Resolving Conflicts

You have several options:

  1. Update constraints to find compatible versions:
package-a = "^1.5"  # Maybe newer version has different requirements
package-b = "^2.0"  # Maybe newer version is compatible
  1. Use different versions of conflicting packages:
package-a = "^1.0"
package-b = "^1.0"
package-c = "^2.0"  # Explicitly specify compatible version
  1. Check for updates to the conflicting packages:
poetry update

Building and Publishing Packages

Poetry simplifies building and publishing Python packages.

Building Distributions

Build both wheel and source distributions:

poetry build

This creates:

dist/
โ”œโ”€โ”€ my_project-0.1.0-py3-none-any.whl
โ””โ”€โ”€ my_project-0.1.0.tar.gz

Build only a wheel:

poetry build --format wheel

Build only a source distribution:

poetry build --format sdist

Publishing to PyPI

Configure your PyPI credentials:

poetry config pypi-token.pypi your-pypi-token

Or use interactive authentication:

poetry publish

This will:

  1. Build distributions
  2. Upload to PyPI
  3. Make your package available for installation via pip install my-project

Publishing to Private Repositories

Configure a private repository:

poetry config repositories.private https://your-private-repo.com/simple/
poetry config pypi-token.private your-token

Publish to the private repository:

poetry publish -r private

Best Practices with Poetry

1. Use Semantic Versioning for Constraints

Use caret (^) for most dependencies:

[tool.poetry.dependencies]
flask = "^3.0.0"      # >=3.0.0, <4.0.0
requests = "^2.31.0"  # >=2.31.0, <3.0.0

Use tilde (~) for more conservative updates:

flask = "~3.0.0"  # >=3.0.0, <3.1.0

Use exact versions only for critical dependencies:

python = "3.11.0"  # Exact Python version

2. Organize Dependencies by Purpose

Use dependency groups for different purposes:

[tool.poetry.dependencies]
python = "^3.9"
flask = "^3.0.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
black = "^23.12.0"

[tool.poetry.group.docs.dependencies]
sphinx = "^7.0.0"

[tool.poetry.group.ci.dependencies]
coverage = "^7.0.0"

Install specific groups:

poetry install --with dev,docs
poetry install --only dev

3. Commit poetry.lock to Version Control

Always commit the lock file:

git add poetry.lock
git commit -m "Update dependencies"

This ensures reproducible environments across all machines.

4. Use Python Version Constraints

Specify supported Python versions:

[tool.poetry.dependencies]
python = "^3.9"  # Supports 3.9, 3.10, 3.11, etc.

Or be more specific:

python = ">=3.9,<3.12"  # Supports 3.9, 3.10, 3.11

5. Document Entry Points

Define CLI commands clearly:

[tool.poetry.scripts]
my-cli = "my_project.cli:main"
my-server = "my_project.server:run"

Users can then run:

pip install my-project
my-cli --help
my-server

6. Use Optional Dependencies

For features that require additional packages:

[tool.poetry.extras]
database = ["sqlalchemy", "psycopg2"]
redis = ["redis"]

[tool.poetry.dependencies]
sqlalchemy = {version = "^2.0", optional = true}
psycopg2 = {version = "^2.9", optional = true}
redis = {version = "^5.0", optional = true}

Users can install with extras:

pip install my-project[database]
pip install my-project[database,redis]

7. Keep pyproject.toml Clean

Organize sections logically:

[tool.poetry]
# Metadata first
name = "my-project"
version = "0.1.0"
description = "..."
authors = ["..."]

[tool.poetry.dependencies]
# Production dependencies

[tool.poetry.group.dev.dependencies]
# Development dependencies

[tool.poetry.scripts]
# Entry points

[tool.poetry.extras]
# Optional features

[build-system]
# Build configuration

8. Use Poetry in CI/CD

In GitHub Actions:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: "3.11"
      - uses: snok/install-poetry@v1
      - run: poetry install
      - run: poetry run pytest
      - run: poetry run black --check .

Common Poetry Workflows

Workflow 1: Starting a New Project

# Create project
poetry new my-project
cd my-project

# Add dependencies
poetry add flask sqlalchemy

# Add development dependencies
poetry add --group dev pytest black flake8

# Create virtual environment and install
poetry install

# Start developing
poetry shell
python -m flask run

Workflow 2: Converting Existing Project

# Initialize Poetry
poetry init

# Add existing dependencies
poetry add flask requests sqlalchemy

# Add development dependencies
poetry add --group dev pytest black

# Install everything
poetry install

# Commit lock file
git add poetry.lock
git commit -m "Add Poetry configuration"

Workflow 3: Collaborating with Team

# Developer A: Add new dependency
poetry add new-package
git add pyproject.toml poetry.lock
git commit -m "Add new-package"
git push

# Developer B: Pull changes
git pull
poetry install  # Installs exact versions from poetry.lock

# Everyone has identical environment

Workflow 4: Publishing a Package

# Update version
poetry version patch  # 0.1.0 -> 0.1.1

# Build distributions
poetry build

# Publish to PyPI
poetry publish

# Tag release
git tag v0.1.1
git push --tags

Troubleshooting Common Issues

Issue 1: “Poetry command not found”

Problem: Poetry is not in your PATH.

Solution:

# Add Poetry to PATH (macOS/Linux)
export PATH="$HOME/.local/bin:$PATH"

# Or reinstall Poetry
curl -sSL https://install.python-poetry.org | python3 -

Issue 2: “No module named poetry”

Problem: Poetry is installed but Python can’t find it.

Solution:

# Reinstall Poetry
pip install --upgrade poetry

# Or use the official installer
curl -sSL https://install.python-poetry.org | python3 -

Issue 3: Dependency Resolution Fails

Problem: Poetry can’t find compatible versions.

Solution:

# Check for conflicts
poetry lock --no-update

# Try updating all dependencies
poetry update

# Relax version constraints
# Change ^1.0.0 to ^1.0 or >=1.0.0

Issue 4: Virtual Environment Issues

Problem: Poetry’s virtual environment is corrupted.

Solution:

# Remove the virtual environment
poetry env remove python3.11

# Recreate it
poetry install

# Or use a specific Python version
poetry env use python3.11

Issue 5: Slow Dependency Resolution

Problem: Poetry takes a long time to resolve dependencies.

Solution:

# Use experimental installer (faster)
poetry config installer.modern-installation true

# Or try without updating lock file
poetry install --no-update

Issue 6: Package Not Found on PyPI

Problem: Poetry can’t find a package.

Solution:

# Check package name (case-sensitive)
poetry add Flask  # Correct
poetry add flask  # Also works (Poetry normalizes names)

# Check if package exists
poetry search package-name

# Try specifying a version
poetry add package-name==1.0.0

When to Use Poetry

Poetry Is Great For:

โœ… New Python projects - Start with Poetry from day one
โœ… Package development - Simplifies building and publishing
โœ… Team projects - Ensures everyone has identical environments
โœ… Complex dependencies - Excellent conflict resolution
โœ… Modern Python development - Follows current best practices
โœ… CI/CD pipelines - Integrates well with automation

Consider Alternatives If:

โŒ Supporting very old Python versions - Poetry requires Python 3.7+
โŒ Minimal tooling needed - pip + requirements.txt might be simpler
โŒ Conda ecosystem - Use conda for data science projects
โŒ Legacy projects - Existing setup.py workflows might be easier to maintain


Conclusion

Poetry represents a significant step forward in Python packaging. By unifying dependency management, virtual environment handling, and package building into a single tool, Poetry eliminates much of the complexity that has plagued Python development.

Key takeaways:

  1. Poetry solves real problems - Dependency conflicts, reproducibility, and tool fragmentation
  2. pyproject.toml is the standard - Modern Python packaging uses this format
  3. Lock files ensure reproducibility - Always commit poetry.lock to version control
  4. Dependency resolution is intelligent - Poetry detects and prevents conflicts
  5. Simple commands for common tasks - poetry add, poetry install, poetry publish
  6. Great for teams - Ensures everyone has identical environments
  7. Modern best practices - Follows PEP standards and current conventions

Whether you’re starting a new project, converting an existing one, or publishing a package to PyPI, Poetry provides a modern, elegant solution to Python’s packaging challenges. Give it a try on your next project, and you’ll likely find yourself reaching for it again and again.

Happy packaging! ๐Ÿ๐Ÿ“ฆ


Additional Resources

Comments