- Poetry installed
- TestPyPI account with 2FA enabled
- TestPyPI API token
Create ~/.pypirc:
[distutils]
index-servers = testpypi
[testpypi]
username = __token__
password = pypi-<your-token-here>Restrict file permissions:
chmod 600 ~/.pypircpoetry add --group dev build "twine>=6.0"my_package/
├── src/
│ └── my_package/
│ └── __init__.py
├── tests/
├── README.md
├── pyproject.toml
└── poetry.lock
[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",
]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__)".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 installCheck that pyproject.toml has all required fields: name, version, description,
authors, readme, license, and requires-python.
TestPyPI will reject malformed README content. Check it renders as valid Markdown or reStructuredText before building.
make buildThis produces dist/<name>-<version>.tar.gz and dist/<name>-<version>-py3-none-any.whl.
make checkAll items should pass before uploading. Fix any errors before proceeding.
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 -20make uploadVisit https://test.pypi.org/project/<your-package-name>/ to confirm the package
page renders correctly.
| 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 |