Python Packaging and Distribution with Poetry

Python Packaging and Distribution with Poetry

Packaging a Python project properly ensures that other developers can install, use, and contribute to your code without dependency conflicts or missing files. Poetry is a modern dependency management and packaging tool that simplifies the entire workflow — from project creation to publishing on PyPI. Unlike pip and setuptools, Poetry uses a declarative pyproject.toml file, resolves dependencies with a SAT solver to avoid version conflicts, and generates deterministic installs via a lock file. This article walks through creating, building, and publishing a Python package with Poetry.

Creating a New Project

Starting a new project with Poetry is a single command. It creates the directory structure, initializes a Git repository, and generates a pyproject.toml file with sensible defaults. The generated structure includes a source directory named after your project, a README.md, and a tests directory.

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

# Project structure created:
# my-project/
#   pyproject.toml
#   README.md
#   my_project/
#       __init__.py
#   tests/
#       __init__.py
#       test_my_project.py

If you are adding Poetry to an existing project instead of starting fresh, run poetry init and answer the prompts. Poetry will generate a pyproject.toml based on your existing requirements.txt or setup.py if you point it at the right files.

Managing Dependencies

Poetry uses a pyproject.toml file (defined in PEP 518 and PEP 621) to declare project metadata and dependencies. Dependencies are organized into groups: the main [tool.poetry.dependencies] section for runtime dependencies, and [tool.poetry.group.dev.dependencies] for development-only packages like test runners, linters, and type checkers. When you run poetry add, Poetry automatically resolves all dependency versions to ensure compatibility and records the exact versions in a poetry.lock file. This lock file should be committed to version control so that everyone working on the project gets identical dependency trees.

[tool.poetry]
name = "my-project"
version = "0.1.0"
description = "A sample Python project"
authors = ["Your Name <you@example.com>"]
readme = "README.md"
license = "MIT"

[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.28"
click = "^8.1"

[tool.poetry.group.dev.dependencies]
pytest = "^7.0"
black = "^22.0"
mypy = "^1.0"
ruff = "^0.1"

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

The ^ operator in version constraints means “compatible with.” For example, ^2.28 allows any version from 2.28 up to but not including 3.0.0. This gives you bug fixes and minor features without risking breaking changes from a major version bump. The python = "^3.10" constraint means your package supports Python 3.10, 3.11, 3.12, etc., but not Python 4.0.

Adding and Removing Dependencies

The Poetry CLI provides intuitive commands for managing dependencies. Each command updates both pyproject.toml and poetry.lock automatically, ensuring your environment stays synchronized with the declared dependencies.

# Add runtime dependencies
poetry add fastapi uvicorn

# Add development-only dependencies
poetry add --group dev mypy pytest-cov

# Remove a dependency
poetry remove requests

# Update all dependencies to latest allowed versions
poetry update

# Show dependency tree
poetry show --tree

# Export to requirements.txt format
poetry export -f requirements.txt --output requirements.txt

The poetry show --tree command is invaluable for debugging dependency conflicts — it displays a tree of every package and its sub-dependencies, making it easy to spot situations where two packages require incompatible versions of the same library.

Building and Publishing

Once your project is ready, building distributable archives is a single command. Poetry produces both a source distribution (.tar.gz) and a wheel (.whl) in the dist/ directory. Wheels are the preferred distribution format because they install faster — they are pre-built and do not require running setup.py. Publishing to PyPI is equally simple. You will need a PyPI API token for authentication instead of a username and password.

# Build source distribution and wheel
poetry build

# Publish to PyPI
poetry publish --username __token__ --password pypi-xxxxxxxxxxxxxxxxxxxx

# Publish to Test PyPI first (recommended)
poetry config repositories.testpypi https://test.pypi.org/legacy/
poetry publish -r testpypi --username __token__ --password pypi-xxxx

Version Management

Poetry includes a built-in version command that follows semantic versioning conventions. It updates both the pyproject.toml version field and creates a Git tag.

# Check current version
poetry version

# Bump version (patch, minor, major, prepatch, preminor, premajor)
poetry version patch   # 0.1.0 -> 0.1.1
poetry version minor   # 0.1.0 -> 0.2.0
poetry version major   # 0.1.0 -> 1.0.0

# Pre-release versions
poetry version prepatch  # 0.1.0 -> 0.1.1a0

By integrating Poetry into your workflow, you get reproducible builds, clean dependency resolution, and a straightforward publishing pipeline — all essential for maintaining a professional Python package.

Publishing to PyPI and CI/CD Integration

Once your package is configured with Poetry, publishing to PyPI is a single command: poetry publish. For automated publishing, configure PyPI tokens as CI/CD secrets. A GitHub Actions workflow can run tests, build with poetry build, publish to TestPyPI on PR merges, and publish to PyPI on version tags. Poetry’s version command (poetry version patch/minor/major) bumps versions according to semantic versioning. The pyproject.toml build-system requires poetry-core ensures pip can install directly from the repository. Poetry’s dependency resolver avoids version conflicts that plague setuptools/pip projects.

Leave a Reply

Your email address will not be published. Required fields are marked *