Skip to content

Instantly share code, notes, and snippets.

@txoof
Last active April 21, 2026 11:56
Show Gist options
  • Select an option

  • Save txoof/37e2678701aaed6a9988f21bc50d4da8 to your computer and use it in GitHub Desktop.

Select an option

Save txoof/37e2678701aaed6a9988f21bc50d4da8 to your computer and use it in GitHub Desktop.
Publishing to PyPi with Poetry

Publishing a Python Package to TestPyPI with Poetry

Prerequisites

  • Poetry installed
  • TestPyPI account with 2FA enabled
  • TestPyPI API token

One-time Setup

1. Configure credentials

Create ~/.pypirc:

[distutils]
index-servers = testpypi

[testpypi]
username = __token__
password = pypi-<your-token-here>

Restrict file permissions:

chmod 600 ~/.pypirc

2. Install dev dependencies

poetry add --group dev build "twine>=6.0"

Project Structure

my_package/
├── src/
│   └── my_package/
│       └── __init__.py
├── tests/
├── README.md
├── pyproject.toml
└── poetry.lock

pyproject.toml Template

[project]
name = "my-package"
version = "0.1.0"
description = "A short description"
authors = [
    {name = "Your Name", email = "you@example.com"}
]
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
classifiers = [
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
dependencies = []

[project.urls]
Homepage = "https://github.com/you/my-package"
Repository = "https://github.com/you/my-package"

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
packages = [{include = "my_package", from = "src"}]

[dependency-groups]
dev = [
    "twine>=6.0",
    "pytest>=9.0.0,<10.0.0",
]

Version String

To avoid maintaining a version string in multiple places, read it dynamically from the installed package metadata in src/my_package/__init__.py:

from importlib.metadata import version

__version__ = version("my-package")

This always reflects the version in pyproject.toml with no manual syncing.

After bumping the version in pyproject.toml, run poetry install to reinstall the package so the dynamic version is picked up:

poetry version patch
poetry install
poetry run python -c "import my_package; print(my_package.__version__)"

Makefile Targets

.PHONY: build upload check bump-patch bump-minor bump-major

build:
	rm -rf dist/
	poetry run python -m build

check:
	poetry run twine check dist/*

upload:
	poetry run twine upload --repository testpypi dist/*

bump-patch:
	poetry version patch
	poetry install

bump-minor:
	poetry version minor
	poetry install

bump-major:
	poetry version major
	poetry install

Build and Publish Workflow

1. Verify metadata before building

Check that pyproject.toml has all required fields: name, version, description, authors, readme, license, and requires-python.

2. Verify README renders correctly

TestPyPI will reject malformed README content. Check it renders as valid Markdown or reStructuredText before building.

3. Clean and build

make build

This produces dist/<name>-<version>.tar.gz and dist/<name>-<version>-py3-none-any.whl.

4. Check the distribution

make check

All items should pass before uploading. Fix any errors before proceeding.

5. Inspect the wheel metadata

If twine check reports missing fields despite them being present in pyproject.toml, inspect the built metadata directly:

unzip -p dist/*.whl '*.dist-info/METADATA' | head -20

6. Upload to TestPyPI

make upload

7. Verify the upload

Visit https://test.pypi.org/project/<your-package-name>/ to confirm the package page renders correctly.

Common Errors

Error Cause Fix
Metadata is missing required fields Twine < 6.0 does not support Metadata-Version 2.4 Upgrade to twine>=6.0
400 Bad Request Malformed README or duplicate version already on TestPyPI Fix README formatting or bump version
File contains no section headers Malformed ~/.pypirc Ensure [distutils] block is present
Source does not appear to be a Python project Running build from the wrong directory Run from the project root where pyproject.toml lives
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment