Skip to main content

How to Package and Publish Your Python Code to PyPI

Intermediate25 min5 exercises75 XP
0/5 exercises

Imagine you've written a fantastic recipe. Right now it only lives in your kitchen notebook. To share it with the world, you'd need to format it nicely, package it into a cookbook, and get it onto bookstore shelves.

Publishing a Python package follows the same idea. You take your working code, add some metadata (title, description, version), package it up, and upload it to PyPI — the Python Package Index. Once it's there, anyone in the world can install it with a simple pip install your-package.

In this tutorial, you'll learn the complete process: writing pyproject.toml, structuring your project for publishing, building distribution files, uploading to PyPI, and managing versions.

What Is pyproject.toml and Why Do You Need It?

Every published package needs a file that describes it — the name, version, author, and what other packages it depends on. In modern Python, that file is pyproject.toml.

Think of pyproject.toml as the label on a product at the store. It tells pip everything it needs to know: what the package is called, who made it, what version it is, and what other packages are required.

Anatomy of a pyproject.toml file
Loading editor...

The [build-system] section tells pip which tool to use for building your package. The [project] section contains all the metadata that shows up on PyPI when people search for your package.

Important pyproject.toml fields
Loading editor...

How Should You Structure a Project for Publishing?

A publishable Python project follows a specific layout. Getting this right from the start saves headaches later.

Recommended project layout for PyPI
Loading editor...

The src/ layout (putting your package inside a src/ folder) is recommended because it prevents a subtle bug. Without it, Python might accidentally import your local source code instead of the installed package during testing.

Setting up __init__.py for publishing
Loading editor...

How Do You Build a Python Package?

Building a package means converting your source code into a format that pip can install. There are two formats: a source distribution (sdist) and a wheel (bdist_wheel).

A source distribution is like shipping raw ingredients — the user's machine compiles anything that needs compiling. A wheel is like shipping a ready-to-eat meal — it's pre-built and installs much faster.

Building and understanding distribution files
Loading editor...
Source Distribution (sdist)
# .tar.gz file
# Contains raw source code
# May need to compile C extensions
# Slower to install
# Works everywhere as fallback
Wheel (bdist_wheel)
# .whl file (actually a zip)
# Pre-built, ready to install
# No compilation needed
# Installs much faster
# Preferred by pip

How Do You Upload Your Package to PyPI?

Once your package is built, you upload it to PyPI using a tool called twine. Think of twine as the delivery truck that takes your packaged product to the store shelves.

Uploading to PyPI with twine
Loading editor...

You'll need an account on pypi.org and an API token for authentication. Twine will ask for your credentials, or you can configure them in a .pypirc file.

What happens after you publish
Loading editor...

How Should You Version Your Python Package?

Version numbers aren't just random digits — they communicate important information to your users. The most common system is Semantic Versioning (SemVer), which uses three numbers: MAJOR.MINOR.PATCH.

Understanding Semantic Versioning
Loading editor...
Pre-release and special versions
Loading editor...

Practice Exercises

These exercises test your understanding of packaging concepts, version management, and the publishing workflow.

Build Package Metadata
Write Code

Write a function called create_metadata(name, version, description) that returns a dictionary with three keys: 'name', 'version', and 'description', mapped to the corresponding arguments. Also include a key 'requires-python' with value '>=3.8' and a key 'license' with value 'MIT'.

Loading editor...
Fix the Version Comparator
Fix the Bug

The code below compares two version strings (like '1.2.3') to determine which is newer. There's a bug causing wrong results. Fix it so compare_versions correctly returns 'newer', 'older', or 'equal'.

Loading editor...
Predict the Build Output
Predict Output

Read the code below that simulates building distribution files for a package. What will it print?

Loading editor...
Refactor the Version Bumper
Refactor

The code below bumps version numbers using repetitive if/elif blocks. Refactor it into a clean function called bump_version(version, part) where part is 'major', 'minor', or 'patch'. The function should return the new version string. When bumping major, reset minor and patch to 0. When bumping minor, reset patch to 0.

Loading editor...
Build a Release Manager
Write Code

Create a class called ReleaseManager that manages package releases:

  • __init__(self, name, version): Store the package name and version (a string like '1.0.0'). Initialize an empty list called releases.
  • bump(self, part): Bump the version by part ('major', 'minor', or 'patch'). When bumping major, reset minor and patch to 0. When bumping minor, reset patch to 0. Store the new version as self.version.
  • release(self): Append a dictionary {'version': self.version, 'files': [sdist, wheel]} to self.releases, where sdist is 'name-version.tar.gz' and wheel is 'name-version-py3-none-any.whl' (replace hyphens with underscores in the name for filenames). Print Released name version.
  • history(self): Print each release on its own line in the format v{version}: {file1}, {file2}.
  • Loading editor...