Publishing Python Packages to PyPI with pyproject.toml
Learn how to publish Python packages to PyPI using pyproject.toml.
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
- ✓Python 3.7 or higher installed
- ✓Basic knowledge of Python packaging concepts
- ✓A PyPI account (free at pypi.org)
- ✓pip and virtual environments
- Use pyproject.toml as the modern standard for package configuration.
- Build distributions with
python -m build. - Upload to PyPI using
twine upload dist/*. - Automate with CI/CD for consistent releases.
- Follow semantic versioning and include a README.
Think of PyPI like the App Store for Python. Publishing a package is like submitting an app: you need a manifest (pyproject.toml) describing your app, build it into a distributable format, and upload it. Once approved, anyone can install it with a single command.
Have you ever written a useful Python utility and wished others could easily install it with pip install your-package? Publishing to PyPI (Python Package Index) makes that possible. In the past, packaging was a maze of setup.py, setup.cfg, and MANIFEST.in. But since PEP 517 and PEP 518, the community has standardized on pyproject.toml as the single source of truth for Python projects. This tutorial walks you through the entire process: from setting up your project structure, writing a robust pyproject.toml, building distributions, and uploading to PyPI. You'll also learn how to avoid common pitfalls, automate releases, and debug issues in production. By the end, you'll be able to share your code with the world confidently.
1. Project Structure and Prerequisites
Before publishing, your project must have a proper structure. A typical Python package looks like this:
`` my_package/ ├── pyproject.toml ├── README.md ├── LICENSE ├── src/ │ └── my_package/ │ ├── __init__.py │ └── core.py └── tests/ └── test_core.py ``
We recommend using a src/ layout to avoid import confusion. The pyproject.toml file is the heart of modern Python packaging. It replaces setup.py, setup.cfg, and MANIFEST.in. You'll need Python 3.7+ and the latest versions of build and twine installed:
``bash pip install build twine ``
Also, create a PyPI account at https://pypi.org and generate an API token for secure uploads.
LICENSE file. Without it, your package is technically not open source, which can deter users.2. Writing a Robust pyproject.toml
The pyproject.toml file uses TOML syntax. The [build-system] table declares the build backend (usually setuptools). The [project] table contains metadata: name, version, authors, description, readme, Python version requirements, license, and classifiers. Classifiers help users discover your package on PyPI. The [project.urls] table links to your project's homepage, repository, or documentation.
For package discovery, use [tool.setuptools.packages.find] with where = ["src"] if you use the src layout. If your package has dependencies, add a dependencies key:
``toml dependencies = [ "requests>=2.25.0", ] ``
You can also specify optional dependencies under [project.optional-dependencies].
3. Building Distributions
Once your pyproject.toml is ready, you can build your package into distributable artifacts. The modern way is to use the build package:
``bash python -m build ``
This creates two files in the dist/ directory: a source distribution (.tar.gz) and a wheel (.whl). The wheel is a pre-built distribution that installs faster. You can also build only the wheel with python -m build --wheel.
Before uploading, validate your distributions with twine check:
``bash twine check dist/* ``
This checks for common issues like missing metadata or broken README rendering. Fix any warnings before proceeding.
4. Uploading to PyPI
To upload, you need a PyPI API token. Create one at https://pypi.org/manage/account/token/. Then use twine:
``bash twine upload dist/* ``
You'll be prompted for username and password. Use __token__ as username and your token as password. To avoid interactive prompts, you can set environment variables:
``bash export TWINE_USERNAME=__token__ export TWINE_PASSWORD=pypi-xxxxxxxx ``
For testing, use TestPyPI first:
``bash twine upload --repository testpypi dist/* ``
Then install from TestPyPI:
``bash pip install --index-url https://test.pypi.org/simple/ my-package ``
Once satisfied, upload to the real PyPI.
5. Automating Releases with CI/CD
Manual publishing is error-prone. Automate with GitHub Actions or other CI tools. Here's a simple workflow that publishes to PyPI when you create a GitHub release:
```yaml name: Publish to PyPI
on: release: types: [published]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build run: python -m build - name: Publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: twine upload dist/* ```
Store your PyPI token in GitHub Secrets as PYPI_API_TOKEN. This workflow triggers on every release, ensuring consistent builds.
6. Best Practices and Common Pitfalls
Best Practices: - Use a src/ layout to prevent import confusion. - Include a comprehensive README with installation, usage, and examples. - Add a license file (MIT, Apache, etc.). - Use semantic versioning and document changes in a CHANGELOG. - Keep dependencies minimal and specify version ranges. - Test your package in a clean virtual environment before publishing.
Common Pitfalls: - Forgetting to include readme in pyproject.toml. - Using a package name that conflicts with an existing one. - Not cleaning the dist/ folder before rebuilding. - Uploading to PyPI without testing on TestPyPI. - Hardcoding API tokens in source code.
Debugging Tips: - Run twine check dist/* to catch metadata issues. - Use pip install -e . to test your package locally. - Check PyPI page after upload to ensure description renders correctly. - If upload fails with 400, check the error message; often it's a duplicate version or invalid name.
The Case of the Missing README
readme = "README.md".readme = "README.md" to the [project] table and rebuilt.- Always explicitly declare the readme file in pyproject.toml.
- Use
twine check dist/*to validate your distribution before uploading. - Test the package by installing it in a fresh virtual environment.
- Automate the build and upload process with CI/CD to avoid manual errors.
- Keep your README up-to-date; it's the first thing users see.
readme is set in pyproject.toml.packages find directive or [tool.setuptools.packages.find].twine check dist/*python -m build| File | Command / Code | Purpose |
|---|---|---|
| pyproject.toml | [build-system] | 1. Project Structure and Prerequisites |
| build_and_check.sh | python -m build | 3. Building Distributions |
| upload.sh | twine upload --repository testpypi dist/* | 4. Uploading to PyPI |
| .github | name: Publish to PyPI | 5. Automating Releases with CI/CD |
| test_install.sh | python -m venv test_env | 6. Best Practices and Common Pitfalls |
Key takeaways
python -m build and validate with twine check.Interview Questions on This Topic
What is the purpose of the `[build-system]` table in pyproject.toml?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
That's Advanced Python. Mark it forged?
3 min read · try the examples if you haven't