Home Python Publishing Python Packages to PyPI with pyproject.toml
Intermediate 3 min · July 14, 2026

Publishing Python Packages to PyPI with pyproject.toml

Learn how to publish Python packages to PyPI using pyproject.toml.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Python 3.7 or higher installed
  • Basic knowledge of Python packaging concepts
  • A PyPI account (free at pypi.org)
  • pip and virtual environments
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is Publishing Python Packages to PyPI with pyproject.toml?

Publishing Python packages to PyPI with pyproject.toml is the process of configuring, building, and uploading a Python package to the Python Package Index using the modern declarative configuration file.

Think of PyPI like the App Store for Python.
Plain-English First

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.

pyproject.tomlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

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

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

[tool.setuptools.packages.find]
where = ["src"]
💡Use src layout
📊 Production Insight
Always include a LICENSE file. Without it, your package is technically not open source, which can deter users.
🎯 Key Takeaway
A clean project structure with a well-formed pyproject.toml is the foundation for successful publishing.

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].

pyproject.tomlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
version = "0.1.0"
description = "A useful utility for data processing"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
    {name = "Jane Doe", email = "jane@example.com"}
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Topic :: Software Development :: Libraries",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "License :: OSI Approved :: MIT License",
]
dependencies = [
    "numpy>=1.21.0",
    "pandas>=1.3.0",
]

[project.urls]
Homepage = "https://github.com/janedoe/my-package"
Documentation = "https://my-package.readthedocs.io"
Repository = "https://github.com/janedoe/my-package"

[tool.setuptools.packages.find]
where = ["src"]
🔥Versioning
📊 Production Insight
Missing classifiers can reduce discoverability. Include at least the Python version and license classifiers.
🎯 Key Takeaway
A comprehensive pyproject.toml includes build system, project metadata, dependencies, and package discovery configuration.

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.

build_and_check.shPYTHON
1
2
3
4
5
# Build distributions
python -m build

# Check distributions
twine check dist/*
Output
Checking dist/my_package-0.1.0-py3-none-any.whl: PASSED
Checking dist/my-package-0.1.0.tar.gz: PASSED
⚠ Clean dist folder
📊 Production Insight
If your package has C extensions, building wheels for multiple platforms requires CI tools like cibuildwheel.
🎯 Key Takeaway
Building creates source and wheel distributions; always validate with twine check before uploading.

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 ``

``bash twine upload --repository testpypi dist/* ``

``bash pip install --index-url https://test.pypi.org/simple/ my-package ``

Once satisfied, upload to the real PyPI.

upload.shPYTHON
1
2
3
4
5
# Upload to TestPyPI
twine upload --repository testpypi dist/*

# Upload to PyPI
twine upload dist/*
Output
Uploading distributions to https://test.pypi.org/legacy/
Uploading my_package-0.1.0-py3-none-any.whl
Uploading my-package-0.1.0.tar.gz
View at:
https://test.pypi.org/project/my-package/0.1.0/
💡Use a .pypirc file
📊 Production Insight
Never hardcode tokens in your repository. Use environment variables or CI/CD secrets.
🎯 Key Takeaway
Upload with twine; always test on TestPyPI first to avoid mistakes on the real index.

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.

.github/workflows/publish.ymlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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/*
🔥Version bump automation
📊 Production Insight
Always run tests before publishing. Add a test step in your workflow to catch bugs early.
🎯 Key Takeaway
CI/CD automation eliminates manual steps and ensures every release is built and uploaded consistently.

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.

test_install.shPYTHON
1
2
3
4
5
6
7
8
9
# Create a fresh virtual environment
python -m venv test_env
source test_env/bin/activate

# Install your package from TestPyPI
pip install --index-url https://test.pypi.org/simple/ my-package

# Test import
python -c "import my_package; print(my_package.__version__)"
Output
0.1.0
⚠ Do not use setup.py
📊 Production Insight
If your package has optional dependencies, document them clearly. Users appreciate knowing what extras are available.
🎯 Key Takeaway
Follow best practices to avoid common mistakes; always test in isolation before publishing.
● Production incidentPOST-MORTEMseverity: high

The Case of the Missing README

Symptom
Users could install the package, but the PyPI page displayed 'No description'.
Assumption
The developer assumed that having a README.md in the root was enough.
Root cause
The pyproject.toml did not include readme = "README.md".
Fix
Added readme = "README.md" to the [project] table and rebuilt.
Key lesson
  • 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.
Production debug guideSymptom to Action4 entries
Symptom · 01
Package installs but has no description on PyPI
Fix
Check if readme is set in pyproject.toml.
Symptom · 02
Upload fails with 'HTTPError: 400 Bad Request'
Fix
Ensure package name is unique and valid on PyPI.
Symptom · 03
Package installs but cannot be imported
Fix
Verify the packages find directive or [tool.setuptools.packages.find].
Symptom · 04
Version conflict when installing
Fix
Check version string format; must follow PEP 440.
★ Quick Debug Cheat SheetCommon publishing issues and immediate fixes.
No description on PyPI
Immediate action
Add `readme = "README.md"` to pyproject.toml
Commands
twine check dist/*
python -m build
Fix now
Rebuild and re-upload.
Upload 400 error+
Immediate action
Check package name on PyPI
Commands
pip install your-package-name
twine upload dist/* --verbose
Fix now
Rename package or use a different version.
ImportError after install+
Immediate action
Check package discovery
Commands
python -c "import your_package"
pip show -f your-package
Fix now
Add [tool.setuptools.packages.find] with correct include.
Featuresetup.pypyproject.toml
Configuration styleImperative (Python code)Declarative (TOML)
StandardizationLegacyPEP 517/518 modern standard
ReadabilityCan be complexSimple and clean
SecurityCan execute arbitrary codeStatic, no code execution
Tool supportWidely supportedModern tools prefer it
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
pyproject.toml[build-system]1. Project Structure and Prerequisites
build_and_check.shpython -m build3. Building Distributions
upload.shtwine upload --repository testpypi dist/*4. Uploading to PyPI
.githubworkflowspublish.ymlname: Publish to PyPI5. Automating Releases with CI/CD
test_install.shpython -m venv test_env6. Best Practices and Common Pitfalls

Key takeaways

1
Use pyproject.toml as the single configuration file for modern Python packaging.
2
Build distributions with python -m build and validate with twine check.
3
Upload to TestPyPI first, then to PyPI using twine.
4
Automate releases with CI/CD to ensure consistency and avoid manual errors.
5
Follow best practices
src layout, semantic versioning, explicit readme, and minimal dependencies.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the purpose of the `[build-system]` table in pyproject.toml?
Q02SENIOR
How do you ensure your package description appears correctly on PyPI?
Q03SENIOR
Explain the difference between source distribution and wheel. When would...
Q01 of 03JUNIOR

What is the purpose of the `[build-system]` table in pyproject.toml?

ANSWER
It specifies the build backend (e.g., setuptools) and its requirements. This tells pip how to build the package from source.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between setup.py and pyproject.toml?
02
How do I update my package on PyPI?
03
Can I publish a package without a wheel?
04
What if my package has C extensions?
05
How do I add dependencies that are only for development?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Advanced Python. Mark it forged?

3 min read · try the examples if you haven't

Previous
Async Patterns with anyio: Backend-Agnostic Async
30 / 35 · Advanced Python
Next
Python Security: SAST, Dependency Scanning, and Best Practices