Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Package Python Apps and Publish to PyPI

Learn to package Python apps and publish to PyPI with hands-on exercises, troubleshooting tips, and next steps.

Focus: package python apps and publish to pypi

Sponsored

So you've built a Python app that actually works — now you want to share it with the world? Manually zipping files or copying code isn't sustainable, and nobody wants to install from a random GitHub repo. This is exactly where packaging your Python app and publishing it to PyPI (Python Package Index) comes in — the official, standardized way to distribute Python software. By the end of this lesson, you'll know how to take your project from a local folder to a pip install-able package on PyPI.

The problem this lesson solves

Distributing Python software has historically been a pain. Think about the last time you needed to share a script with a teammate: you probably zipped a folder, emailed an archive, or pointed them to a Git repo. None of those solutions scale. Users expect a simple, one-line command — pip install yourpackage — to get your code, along with all its dependencies. Without proper packaging, you're stuck managing manual installs, broken imports, and version conflicts.

This lesson tears down that wall. After you've done the work, anyone in the world can install, upgrade, and uninstall your Python app with standard tools.

Core concept / mental model

Think of PyPI as the App Store for Python. Just as you'd submit an iPhone app to the App Store so users can download it with one click, you upload your Python package to PyPI so users can pip install it. The package itself is more than just your code — it's a portable, self-describing collection that includes:

  • Your source files (the actual Python modules)
  • Metadata (name, version, author, description)
  • Dependencies (other packages your code needs)
  • Build instructions (how to assemble the package, often with setuptools)

Pro tip: A Python package is really just a directory with an __init__.py file. The setup.py or pyproject.toml file is the recipe card that tells pip how to install it.

How it works step by step

1. Structure your project

Every distributable Python package follows a standard structure. Here's a minimal example:

my_package/
├── my_package/
│   ├── __init__.py
│   └── core.py
├── tests/
│   └── test_core.py
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore

Your actual code lives in a subdirectory named after your package (e.g., my_package/). The __init__.py file makes Python treat the directory as a package.

2. Write the build configuration

Modern Python packaging uses pyproject.toml (PEP 621) or setup.py. I strongly recommend pyproject.toml — it's the community standard as of Python 3.10+.

[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.10"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

dependencies = [
    "requests>=2.31.0",
]

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

Notice the dependencies list — pip will automatically install requests when someone runs pip install my-package.

3. Build your package

With the configuration in place, install build and create distribution files:

pip install build
ython -m build

This produces two files in dist/: - A source distribution (.tar.gz) - A wheel (.whl) — the faster, preferred format

4. Upload to PyPI

First, create an account on pypi.org and generate an API token. Then install twine:

pip install twine
ython -m twine upload dist/*

Twine will prompt for your username (use __token__) and password (your API token). After a successful upload, your package is live!

Hands-on walkthrough

Let's create a real package from scratch. We'll make a simple utility that greets users.

Step 1: Create the project structure

mkdir greeting-utils
cd greeting-utils
mkdir greeting_utils
ytouch greeting_utils/__init__.py
ytouch greeting_utils/greet.py

Step 2: Write the code

In greeting_utils/greet.py:

def say_hello(name: str) -> str:
    """Return a friendly greeting."""
    return f"Hello, {name}! Welcome to PyPI."


def say_goodbye(name: str) -> str:
    """Return a farewell message."""
    return f"Goodbye, {name}. See you next time!"

In __init__.py:

from .greet import say_hello, say_goodbye

Step 3: Add pyproject.toml

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "greeting-utils"
version = "0.1.0"

description = "A simple greeting utility"
readme = "README.md"
requires-python = ">=3.10"

[project.urls]
Homepage = "https://github.com/yourname/greeting-utils"

Step 4: Build and upload

pip install build twine
ython -m build
ython -m twine upload dist/* --username __token__ --password pypi-xxxxx

After upload:

# Test it in a clean environment
pip install greeting-utils
>>> from greeting_utils import say_hello
>>> print(say_hello("Alice"))
Hello, Alice! Welcome to PyPI.

Pro tip: Always test using pip install -e . in development mode before publishing. This creates an editable install that reflects code changes immediately.

Compare options / when to choose what

Approach Best for Complexity Use case
pyproject.toml + setuptools Most modern packages Low Standard Python libraries
setup.py only Legacy projects Medium Maintaining older packages
poetry New projects with dependency management Medium Full-featured package management
flit Simple packages Low Pure Python packages with no build requirements

When to choose what: - Stick with pyproject.toml + setuptools if you want the easiest path to PyPI with maximum compatibility. - Use poetry if you also need dependency resolution and virtual environment management in one tool. - Avoid setup.py alone for new projects — it's being deprecated in favor of pyproject.toml.

Troubleshooting & edge cases

"No module named 'greeting_utils' after install"

  • Cause: Your pyproject.toml may not have [tool.setuptools.packages.find] or you're using the wrong name.
  • Fix: Add packages = ["greeting_utils"] under [tool.setuptools]:
[tool.setuptools]
packages = ["greeting_utils"]

Upload fails with "400: The description failed to render"

  • Cause: Your README.md contains invalid Markdown or HTML that PyPI can't process.
  • Fix: Validate your README locally: python -m twine check dist/*

"ERROR: Could not find an activation context" on Windows

  • Cause: Missing dependencies in console_scripts
  • Fix: Use entry_points in pyproject.toml for command-line scripts:
[project.scripts]
greet = "greeting_utils.greet:main"

Version conflicts after upload

  • Cause: You bumped the version number incorrectly.
  • Fix: Follow Semantic Versioning: MAJOR.MINOR.PATCH. Use pip install package==1.0.0 to pin versions.

What you learned & what's next

You now know how to:

  • Structure a Python project for distribution
  • Write a pyproject.toml that defines all package metadata
  • Build your package into distributable artifacts (sdist + wheel)
  • Upload to PyPI using twine
  • Troubleshoot common packaging pitfalls

What's next? Now that your package is on PyPI, the next lesson covers creating command-line tools with entry points — turning your package into a full-fledged CLI application that users can run directly from their terminal. You'll learn how to define console_scripts and build interactive command-line interfaces.

Practice recap

Create your own small Python utility (e.g., a temperature converter) and publish it to TestPyPI following the steps above. Submit a screenshot of your successful upload to the community forum, along with a link to your package page on test.pypi.org. This will give you confidence for the real PyPI release in the next lesson.

Common mistakes

  • Forgetting to include __init__.py — without it, Python doesn't recognize the directory as a package and imports will fail.
  • Using setup.py alone without pyproject.toml — modern tools (pip 21.3+) strongly prefer the new standard and may warn or fail.
  • Not updating the version number before re-uploading — PyPI rejects duplicate version strings; always bump MAJOR.MINOR.PATCH.
  • Hard-coding paths inside your package — use importlib.resources or pkg_resources for accessing data files instead.

Variations

  1. Use poetry instead of plain setuptools — it handles dependency resolution and virtual environment management automatically.
  2. Publish to TestPyPI first (test.pypi.org) — this lets you validate the upload and install process before going live.
  3. Use flit for ultra-simple packages — it requires minimal configuration and supports pure-Python packages elegantly.

Real-world use cases

  • Distributing an internal analytics library across multiple company projects via a private PyPi mirror or Artifactory.
  • Releasing an open-source CLI tool (e.g., a JSON formatter) that users can install with pip install json-tidy.
  • Publishing a reusable Django or Flask extension (like django-crispy-forms) that adds functionality via plugin systems.

Key takeaways

  • A proper package structure with __init__.py and pyproject.toml is the foundation of Python distribution.
  • Use pyproject.toml over setup.py for new projects — it's the modern, supported standard.
  • Build produces both .tar.gz (sdist) and .whl (wheel) archives; wheels are faster for installation.
  • Always test with pip install -e . in a virtual environment before uploading to PyPI.
  • Version numbers must follow semantic versioning (e.g., 0.1.0, 1.0.0) and must be unique per release.
  • Upload with twine — it's more secure than setup.py upload and validates your package before sending.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.