Welcome back, future Pythonista! In our journey so far, you’ve learned to write amazing Python code, organize it into modules, and even create your own packages. But what if you want to share your brilliant creations with the world? How do you make it easy for others (or your future self!) to install and use your code without manually copying files around?
That’s where packaging and distribution come in! This chapter is all about transforming your Python project into a professional, easily installable package that can be shared on platforms like the Python Package Index (PyPI). We’ll cover the modern tools and best practices to get your code out there, making it reusable and discoverable.
By the end of this chapter, you’ll understand how to structure your project for distribution, configure it using the modern pyproject.toml standard, build distributable files, and even install your own package locally. Get ready to level up your project management skills and become a true open-source contributor!
What Exactly is Python Packaging?
Imagine you’ve baked a fantastic cake. You could just hand someone the recipe and all the raw ingredients, expecting them to bake it themselves. Or, you could package it beautifully in a box, with clear instructions, ready for them to enjoy!
Python packaging is like putting your “cake” (your Python code) into a neat “box” (a distributable package) along with all the “instructions” (metadata, dependencies) so that others can easily “eat” it (install and use it). Instead of sharing individual .py files, which can be messy and hard to manage, you create a self-contained unit that pip (Python’s package installer) knows how to handle.
Why Bother Packaging?
- Reusability: Make your code easily usable across different projects and by different people.
- Dependency Management: Clearly define what other libraries your project needs, and
pipwill handle installing them automatically. - Standardization: Follows community-wide conventions, making your project familiar to other Python developers.
- Distribution: Easily upload your package to repositories like PyPI, making it discoverable and installable by anyone with
pip. - Professionalism: A well-packaged project looks more polished and reliable.
Prerequisites for This Chapter
We’ll assume you’re comfortable with:
- Creating and running Python scripts.
- Understanding Python modules and basic package structure (
__init__.py). - Using the command line (terminal/PowerShell/CMD).
- Working with
pipand virtual environments (which we covered in a previous chapter – remember how important they are!).
The Modern Python Packaging Landscape (2025)
The Python packaging world has evolved significantly, and as of December 2025 (with Python 3.14.1 being the latest stable release), the ecosystem is more robust and standardized than ever.
Key Players: pyproject.toml, build, and pip
pyproject.toml: This is the heart of modern Python project configuration. It’s a TOML (Tom’s Obvious, Minimal Language) file that stores all the metadata about your project (name, version, authors, description, dependencies) and tells build tools how to build your package. It’s designed to be a universal configuration file, standardizing settings across different tools.- Why
pyproject.toml? It replaces older files likesetup.py(which was Python code) andsetup.cfg(an INI-style file) for defining project metadata. This separation makes configuration declarative and easier for tools to parse without executing arbitrary Python code.
- Why
build: This is the recommended tool for creating distributable files from your project. It’s a simple, standardized frontend for various backend build systems (likesetuptools, which it often uses under the hood).- Why
build? It simplifies the process of creating “source distributions” (sdist) and “wheels” (bdist_wheel), which are the two primary formats for distributing Python packages. It ensures your packages are built correctly and consistently. You install it viapip install build.
- Why
pip: Our old friend! Once your package is built,pipis what users will use to install it (e.g.,pip install your-package-name). You’ll also usepipto install thebuildtool itself.
Types of Distributions
When you package your project, you’ll typically create two types of files:
- Source Distribution (
.tar.gzor.zip): This package contains your source code and any necessary metadata. It’s essentially a compressed archive of your project, and the user’spipinstallation will build the package from source on their machine. - Wheel (
.whl): This is a pre-built distribution format. Wheels are “ready-to-install” packages that don’t require any compilation steps during installation, making them much faster to install. They are platform-specific if your package contains compiled C extensions, but for pure Python packages, they are platform-independent. For pure Python, a wheel is generally preferred.
Step-by-Step Implementation: Packaging a Simple Greeter
Let’s get our hands dirty! We’ll create a very simple Python package called my-greeter-package that offers a friendly greeting.
Step 1: Project Setup
First, let’s create a dedicated directory for our project and set up a standard src layout. The src layout is a modern best practice where your actual Python package code lives inside a src subdirectory. This helps prevent issues with accidentally importing your package’s code from the current directory during development instead of the installed version.
Create the main project directory: Open your terminal or command prompt and run:
mkdir my-greeter-package cd my-greeter-packageCreate the
srcdirectory and your package: Insidemy-greeter-package, create thesrcdirectory, and insidesrc, create your actual Python package directory,greeter.mkdir src mkdir src/greeterCreate the
__init__.pyfile: This file tells Python that thegreeterdirectory is a Python package. It can be empty, but it’s often used to define what symbols are exposed when the package is imported. Create a file named__init__.pyinsidesrc/greeter/and add the following content:# File: my-greeter-package/src/greeter/__init__.py # This makes 'greeter' a Python package. # We can also define what gets imported by default here. from .greetings import greet_user __version__ = "0.1.0" # Define a version for our package- Explanation:
from .greetings import greet_user: This line makesgreet_userdirectly accessible when someone importsgreeter. For example, afterimport greeter, they can callgreeter.greet_user(). The.means “from the current package”.__version__ = "0.1.0": It’s good practice to define a__version__string in your__init__.pyfile. This version will be synchronized with the version defined inpyproject.toml.
- Explanation:
Create your main module file: Inside
src/greeter/, create a file namedgreetings.pyand add your simple greeting function:# File: my-greeter-package/src/greeter/greetings.py def greet_user(name="World"): """ Returns a friendly greeting for the given name. If no name is provided, it greets the 'World'. """ return f"Hello, {name}! Welcome to Python packaging!" if __name__ == "__main__": print(greet_user("Learner"))- Explanation: This is a straightforward function. The
if __name__ == "__main__":block allows you to test the module directly if you runpython greetings.py, but it won’t execute when the module is imported as part of the package.
- Explanation: This is a straightforward function. The
Your project structure should now look like this:
my-greeter-package/
├── src/
│ └── greeter/
│ ├── __init__.py
│ └── greetings.py
Step 2: Define pyproject.toml
Now, let’s create the pyproject.toml file in the root of your my-greeter-package directory. This file will contain all the essential metadata for your package.
Create pyproject.toml in the my-greeter-package directory (at the same level as src).
# File: my-greeter-package/pyproject.toml
# This section defines the build system requirements.
[build-system]
requires = ["setuptools>=61.0.0", "wheel"] # Tools needed to build the package
build-backend = "setuptools.build_meta" # The actual build backend to use
# This section contains general project metadata.
[project]
name = "my-greeter-package"
version = "0.1.0" # This should match __version__ in __init__.py
description = "A simple Python package to greet users."
readme = "README.md" # Path to your project's README file
requires-python = ">=3.8" # Minimum Python version required
license = { file = "LICENSE" } # Path to your license file
keywords = ["greeter", "hello", "packaging", "example"]
authors = [
{ name = "AI Expert", email = "ai@example.com" },
]
classifiers = [ # Standard PyPI classifiers for categorization
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [ # Any external packages your project depends on
# e.g., "requests>=2.20.0",
]
# This section is for optional URLs related to your project.
[project.urls]
Homepage = "https://github.com/yourusername/my-greeter-package" # Replace with your repo
"Bug Tracker" = "https://github.com/yourusername/my-greeter-package/issues"
- Explanation (Breaking down
pyproject.toml):[build-system]section:requires: Lists the packages thatpipneeds to install before it can build your project.setuptoolsis the workhorse here, andwheelis needed to create.whlfiles. We specifysetuptools>=61.0.0as a modern, stable version.build-backend: Specifies which toolbuildshould use to actually perform the build.setuptools.build_metais the standard forsetuptools-based projects.
[project]section: This is where you declare all the public metadata about your package.name: The name of your package on PyPI. It should be unique.version: The current version of your package. Follows semantic versioning (e.g.,MAJOR.MINOR.PATCH). Crucially, this should match the__version__you defined insrc/greeter/__init__.py.description: A short, one-line summary.readme: Points to your project’sREADME.mdfile, which will be displayed on PyPI. (We’ll create this next!)requires-python: The minimum Python version your package supports. We’re using Python 3.14.1, so3.8is a safe lower bound for modern practices.license: Specifies the license under which your code is released. We’re pointing to aLICENSEfile. (We’ll create this next too!)keywords: Search terms to help users find your package.authors: Information about the project authors.classifiers: A list of standard strings from PyPI that categorize your project. These are super important for discoverability! You can find a full list at https://pypi.org/classifiers/.dependencies: A list of other Python packages your project requires to run.pipwill automatically install these when your package is installed. Our simple greeter doesn’t have any external dependencies, so it’s empty for now.
[project.urls]section: Optional but highly recommended for linking to your project’s homepage, bug tracker, etc.
Step 3: Add README.md and LICENSE
To make our pyproject.toml happy and provide good documentation, let’s quickly create a README.md and a LICENSE file in the root my-greeter-package directory.
Create
README.md:# File: my-greeter-package/README.md # My Greeter Package A simple Python package that provides a friendly greeting function. ## Installation ```bash pip install my-greeter-packageUsage
from greeter import greet_user print(greet_user("Alice")) # Output: Hello, Alice! Welcome to Python packaging! print(greet_user()) # Output: Hello, World! Welcome to Python packaging!Development
To install for development:
git clone https://github.com/yourusername/my-greeter-package.git cd my-greeter-package python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install -e .* **Explanation:** A good `README` explains what your project does, how to install it, and how to use it. This is crucial for anyone encountering your package.Create
LICENSE(MIT License example):# File: my-greeter-package/LICENSE MIT License Copyright (c) 2025 AI Expert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.- Explanation: A license file specifies the legal terms under which your software can be used, modified, and distributed. The MIT License is a popular, permissive open-source license.
Your project structure now looks like this:
my-greeter-package/
├── src/
│ └── greeter/
│ ├── __init__.py
│ └── greetings.py
├── pyproject.toml
├── README.md
└── LICENSE
Step 4: Building Your Package
Now that our project is configured, let’s use the build tool to create our distributable files.
Install the
buildpackage: First, ensure you havepipandbuildinstalled. It’s always a good idea to do this inside a virtual environment to keep your global Python installation clean.# Create a virtual environment (if you haven't already) python3.14 -m venv .venv # Activate the virtual environment # On macOS/Linux: source .venv/bin/activate # On Windows: # .venv\Scripts\activate # Install the build tool (and twine, which we'll mention later) pip install build twine- Explanation: We’re explicitly using
python3.14to ensure we’re targeting the latest Python version.venvcreates an isolated environment.pip install build twineinstalls the necessary tools within this environment.
- Explanation: We’re explicitly using
Run the
buildcommand: With your virtual environment activated, navigate to yourmy-greeter-packageroot directory (wherepyproject.tomlis) and run:python -m build- Explanation:
python -m buildtells Python to run thebuildmodule. This command will read yourpyproject.tomlfile, execute thesetuptoolsbackend, and create your distribution files.
After running the command, you should see output similar to this (details might vary slightly):
* Creating venv isolated build environment... * Installing packages in isolated environment... (setuptools, wheel) * Getting dependencies for sdist... * Building sdist (wheel) * Building wheel... Successfully built my_greeter_package-0.1.0-py3-none-any.whl and my_greeter_package-0.1.0.tar.gzYou’ll notice a new directory named
dist/has been created in your project root. Insidedist/, you’ll find your two distribution files:my_greeter_package-0.1.0.tar.gz(the source distribution)my_greeter_package-0.1.0-py3-none-any.whl(the wheel distribution)
my-greeter-package/ ├── src/ │ └── greeter/ │ ├── __init__.py │ └── greetings.py ├── dist/ │ ├── my_greeter_package-0.1.0.tar.gz │ └── my_greeter_package-0.1.0-py3-none-any.whl ├── pyproject.toml ├── README.md └── LICENSE- Explanation:
Step 5: Installing and Testing Your Package Locally
Now that you have your distributable files, let’s test them out! We’ll install the wheel file into a new virtual environment to simulate a fresh installation.
Create a new, separate virtual environment: It’s good practice to test your package in an environment completely separate from your development environment. Navigate out of your
my-greeter-packagedirectory, then create a new directory and a new venv.cd .. # Go up one level from my-greeter-package mkdir test_greeter_install cd test_greeter_install python3.14 -m venv .venv_test source .venv_test/bin/activate # On Windows: .venv_test\Scripts\activate- Explanation: We’ve created a fresh, empty environment.
Install your package using
pip: Now, from within your activatedvenv_testenvironment, usepipto install the wheel file. You’ll need to provide the full path to the.whlfile.pip install ../my-greeter-package/dist/my_greeter_package-0.1.0-py3-none-any.whl- Explanation:
../my-greeter-package/dist/points to thedistdirectory of your project.pipdirectly installs the wheel file.
You should see output indicating successful installation.
- Explanation:
Test your installed package: Now that it’s installed, you can import and use your
greeterpackage just like any other installed library!python(This opens the Python interactive interpreter)
>>> import greeter >>> greeter.greet_user("Learner") 'Hello, Learner! Welcome to Python packaging!' >>> greeter.greet_user() 'Hello, World! Welcome to Python packaging!' >>> greeter.__version__ '0.1.0' >>> exit() # Type this to exit the Python interpreter- Explanation: We successfully imported
greeterand used itsgreet_userfunction. We also accessed the__version__attribute, confirming our package metadata is correctly applied. Fantastic!
- Explanation: We successfully imported
Step 6: (Optional) Uploading to PyPI
While we won’t perform a live upload in this guide, it’s important to know the next steps for sharing your package with the world.
Create an account on PyPI (or TestPyPI):
- For practice, use TestPyPI: https://test.pypi.org/
- For actual public packages, use PyPI: https://pypi.org/
Use
twineto upload:twineis the secure and recommended tool for uploading your built distributions to PyPI. You already installed it in Step 4.# Make sure your main project's virtual environment is activated # (the one where you ran python -m build) # For TestPyPI: python -m twine upload --repository testpypi dist/* # For official PyPI: # python -m twine upload dist/*- Explanation:
twinewill prompt you for your username and password (or an API token, which is more secure). It then securely uploads your.tar.gzand.whlfiles from thedist/directory.
Once uploaded, anyone can install your package using
pip install my-greeter-package(orpip install --index-url https://test.pypi.org/simple/ my-greeter-packagefor TestPyPI).- Explanation:
Mini-Challenge: Enhance Your Greeter
You’ve successfully built and installed your first package! Now, let’s make a small enhancement and go through the update process.
Challenge:
- Modify
src/greeter/greetings.pyto add a new function calledfarewell_user(name="Friend")that returns a goodbye message. - Update
src/greeter/__init__.pyto expose this newfarewell_userfunction. - Crucially, increment the
versionin bothsrc/greeter/__init__.pyandpyproject.tomlto0.1.1(or higher). - Rebuild your package using
python -m build. - Go back to your
test_greeter_installvirtual environment, uninstall the old version of your package, and install the new one. - Verify that both
greet_userandfarewell_userwork, and thatgreeter.__version__reflects the new version.
Hint:
- Remember to
deactivateyour virtual environment if you need to switch betweenmy-greeter-packageandtest_greeter_installenvironments. - To uninstall, use
pip uninstall my-greeter-package. - Ensure you rebuild in the
my-greeter-packagedirectory and install the new.whlfile intest_greeter_install.
What to Observe/Learn:
This challenge reinforces the entire packaging workflow and demonstrates how easily you can update and redeploy your packages. It highlights the importance of versioning and the smooth update process pip provides.
(Pause here, try the challenge!)
…
Common Pitfalls & Troubleshooting
Packaging can sometimes feel a bit finicky, especially when starting out. Here are some common issues and how to resolve them:
Missing
__init__.py:- Pitfall: If you forget to create an
__init__.pyfile in a directory, Python won’t recognize it as a package. Your build might fail, or your package might install but fail to import. - Solution: Always ensure your package directories (like
greeterin our example) contain an__init__.pyfile, even if it’s empty.
- Pitfall: If you forget to create an
Incorrect Paths in
pyproject.toml(especially forsrclayout):- Pitfall: If your
pyproject.tomlisn’t correctly configured for thesrclayout,buildmight not find your package code. For example, if you forgotpackages = ["greeter"](thoughsetuptoolsoften finds them automatically withsrc-layout), orpackage-dir = {"" = "src"}(whichsetuptoolsalso often infers). - Solution: The
setuptoolsbuild backend, when used withpyproject.toml, is smart about thesrclayout. If you stick to thesrc/your_package_namestructure, it usually works out of the box. If you encounter issues, double-check the[tool.setuptools]section inpyproject.tomlfor explicitpackage-dirorpackagesconfigurations. For our simple case, the default[project]section is enough, andsetuptoolshandles thesrclayout well.
- Pitfall: If your
Dependency Issues:
- Pitfall: Forgetting to list a required package in
[project].dependenciesinpyproject.tomlwill lead toModuleNotFoundErrorwhen users try to run your package. - Solution: Always list all direct external dependencies your package needs to function in the
dependenciesarray. Specify version constraints (e.g.,requests>=2.20.0,<3.0.0) for stability.
- Pitfall: Forgetting to list a required package in
Not Using Virtual Environments:
- Pitfall: Installing
buildor your own package directly into your global Python environment can lead to dependency conflicts and a messy system. - Solution: Always use virtual environments! They isolate your project’s dependencies, making development and testing much cleaner and preventing “it works on my machine” syndrome.
- Pitfall: Installing
Version Mismatch:
- Pitfall: If the
versioninpyproject.tomldoesn’t match__version__in your package’s__init__.py, it can cause confusion or lead to tools picking up the wrong version. - Solution: Keep them synchronized. Many projects use tools like
setuptools_scmto automatically determine the version from Git tags, but for simpler projects, manual synchronization is fine.
- Pitfall: If the
Summary
Congratulations! You’ve successfully navigated the modern landscape of Python packaging and distribution. You now have the skills to take your projects from local code to shareable, installable packages.
Here are the key takeaways from this chapter:
- Packaging makes your code reusable and distributable. It’s essential for sharing your work.
pyproject.tomlis the modern standard for configuring your Python projects, defining metadata, and specifying build system requirements.- The
buildtool is the recommended way to create standard distributable files: Source Distributions (.tar.gz) and Wheels (.whl). - The
srclayout (src/your_package_name/) is a best practice for structuring your project. pipis used to install your packaged code, both locally and from repositories like PyPI.- Always use virtual environments for both development and testing to maintain clean, isolated environments.
twineis used to securely upload your built packages to PyPI (or TestPyPI).- Versioning is crucial for managing updates and releases of your package.
What’s Next?
In the next chapter, we’ll dive into another critical aspect of modern Python development: Testing Your Code. Packaging helps you share your code, but robust testing ensures that the code you’re sharing actually works as expected! Get ready to learn how to write effective tests and build confidence in your Python applications.