DEV Community

Yuuichi Eguchi
Yuuichi Eguchi

Posted on

How to Publish a Python Package to PyPI

Introduction

I recently developed and published my first Python library to PyPI. This article is not about the technical details of the library itself, but rather a record of the process from knowing nothing to releasing it.

Published library:

I hope this will be helpful for anyone who wants to create a library but doesn't know where to start.


What Motivated Me

While developing real-time communication applications with FastAPI, I struggled with WebSocket connection instability.

Specifically:

  • Unexpected connection drops
  • No heartbeat mechanism
  • Zombie connection accumulation
  • Complex reconnection handling
  • Cumbersome graceful shutdown implementation

I kept writing similar code to address these issues, and when I checked GitHub Issues and Stack Overflow, I found many developers facing the same challenges.

"Then maybe there's value in publishing this as a general-purpose library."

That's when I started development.


Project Structure

The first challenge was deciding on the project structure. After research, I adopted this standard structure:

project-root/
├── src/
│   └── fastapi_websocket_stabilizer/
│       ├── __init__.py
│       ├── manager.py
│       ├── config.py
│       └── ...
├── tests/
├── README.md
├── LICENSE
└── pyproject.toml
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Used src layout (to use the installed package during testing)
  • Package name with hyphens, module name with underscores
  • Chose MIT license (for wide adoption)

pyproject.toml Configuration

Modern Python projects use pyproject.toml instead of setup.py for configuration. Here's what I used:

[build-system]
requires = ["setuptools>=70.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "fastapi-websocket-stabilizer"
version = "0.1.0"
description = "A production-ready WebSocket stabilization layer for FastAPI applications"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
    {name = "FastAPI WebSocket Stabilizer Contributors"}
]
keywords = [
    "fastapi",
    "websocket",
    "connection-management",
    "heartbeat",
    "graceful-shutdown",
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Environment :: Web Environment",
    "Intended Audience :: Developers",
    "Operating System :: OS Independent",
    "Programming Language :: Python",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Topic :: Internet :: WWW/HTTP",
    "Topic :: Software Development :: Libraries :: Python Modules",
]

dependencies = [
    "fastapi>=0.95.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-asyncio>=0.21.0",
    "pytest-cov>=4.0",
    "black>=23.0",
    "ruff>=0.1.0",
    "mypy>=1.0",
    "uvicorn>=0.20.0",
]

[project.urls]
Homepage = "https://github.com/yuuichieguchi/fastapi-websocket-stabilizer"
Repository = "https://github.com/yuuichieguchi/fastapi-websocket-stabilizer"
Documentation = "https://github.com/yuuichieguchi/fastapi-websocket-stabilizer#readme"
Issues = "https://github.com/yuuichieguchi/fastapi-websocket-stabilizer/issues"

[tool.setuptools]
packages = ["fastapi_websocket_stabilizer"]
package-dir = {"" = "src"}

[tool.black]
line-length = 100
target-version = ["py310", "py311", "py312"]

[tool.ruff]
line-length = 100
target-version = "py310"
select = ["E", "F", "W", "I"]

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_untyped_calls = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
Enter fullscreen mode Exit fullscreen mode

Key points:

  • classifiers specify categorization on PyPI
  • optional-dependencies separate development tools
  • tool.* sections integrate linter, formatter, and test configurations

Setting Up PyPI Account

Creating an Account

  1. Create an account at https://pypi.org
  2. Verify your email address
  3. Enable two-factor authentication (recommended)

Generating an API Token

For security, use an API token instead of a password.

  1. Log in to PyPI
  2. Go to Account settings → API tokens
  3. Click "Add API token"
  4. Select Scope: "Entire account" (for initial publication)
  5. Store the generated token safely

Important: The token is only displayed once, so make sure to save it.

Configuring Authentication

Create a ~/.pypirc file:

[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmc... (your token)
Enter fullscreen mode Exit fullscreen mode

Building the Package

Installing Required Tools

pip install --upgrade build twine setuptools wheel
Enter fullscreen mode Exit fullscreen mode

Building

python -m build
Enter fullscreen mode Exit fullscreen mode

On success, the following files will be generated in the dist/ directory:

  • .whl file (wheel format)
  • .tar.gz file (source distribution format)

Troubleshooting: InvalidDistribution Error

During my first build, I encountered this error:

InvalidDistribution: unrecognized or malformed field 'license-file'
Enter fullscreen mode Exit fullscreen mode

Cause

This was due to version conflicts between twine and packaging libraries.

Solution

# Update related tools
pip install --upgrade packaging build setuptools wheel twine

# Clear cache
rm -rf dist build *.egg-info

# Rebuild
python -m build
Enter fullscreen mode Exit fullscreen mode

This resolved the issue.


Uploading to PyPI

Testing with TestPyPI (Recommended)

Before uploading to production PyPI, I recommend testing with TestPyPI:

python -m twine upload --repository testpypi dist/*
Enter fullscreen mode Exit fullscreen mode

TestPyPI: https://test.pypi.org

Uploading to Production

If everything looks good, upload to production:

python -m twine upload dist/*
Enter fullscreen mode Exit fullscreen mode

After upload completes, your package will be accessible at https://pypi.org/project/your-package-name/ within a few minutes.

Verification

pip install fastapi-websocket-stabilizer
Enter fullscreen mode Exit fullscreen mode

Now anyone in the world can install it with this command.


Post-Publication Maintenance

Version Management

Follow semantic versioning:

  • MAJOR: Breaking changes
  • MINOR: Backward-compatible feature additions
  • PATCH: Backward-compatible bug fixes

Example: 0.1.00.1.1 (bug fix) → 0.2.0 (new feature) → 1.0.0 (stable)

Update Process

  1. Modify code
  2. Update version in pyproject.toml
  3. Update CHANGELOG.md
  4. Rebuild and re-upload
python -m build
python -m twine upload dist/*
Enter fullscreen mode Exit fullscreen mode

Documentation

For users, I prepared:

  • Usage examples and API reference in README.md
  • Type hints for IDE completion
  • Function descriptions via docstrings

Reflections

Technical Learnings

Through library development, I learned:

  • Python packaging mechanisms
  • Importance of type hints
  • Maintaining test coverage
  • Setting up CI/CD

For Those Creating Their First Library

If you're thinking "I want to create a library but it seems daunting," here are my recommendations:

  1. Start small: You don't need to build something huge from the start
  2. Solve real problems: Libraries that solve problems you've faced have value
  3. Practice with TestPyPI: You can test in a staging environment before going live
  4. Write good documentation: Clear usage instructions attract users
  5. Welcome feedback: Issues are opportunities for improvement

The technical hurdles are lower than you might think. The bigger wall is probably "the courage to publish."


Conclusion

Publishing my first library was a valuable experience, not just for technical learning but also for participating in the OSS community.

If you're thinking "I want to try making one," I encourage you to take that first step.

References:


Top comments (0)