Creating a source distribution (sdist) for your Python package is how you package your code so others can build and install it on their systems, even if they don’t have the exact same build tools or environment as you. It’s the most universal way to distribute Python packages.
Let’s see this in action. Imagine you have a simple package named my_package with a single file my_module.py:
# my_package/my_module.py
def greet(name="World"):
return f"Hello, {name}!"
And a setup.py file to define your package:
# setup.py
from setuptools import setup, find_packages
setup(
name="my-package",
version="0.1.0",
packages=find_packages(),
description="A simple example package",
author="Your Name",
author_email="your.email@example.com",
url="http://example.com/my_package",
)
To build an sdist, you’ll use build (the modern tool for building package distributions) or setuptools directly. First, make sure you have build installed:
pip install build
Then, navigate to the root directory of your project (where setup.py is located) and run:
python -m build --sdist
This command will create a dist/ directory. Inside dist/, you’ll find a file like my-package-0.1.0.tar.gz. This is your source distribution. Anyone can take this .tar.gz file and install your package using pip install my-package-0.1.0.tar.gz. Pip will then unpack this archive and run setup.py (or pyproject.toml if you use that) on their machine to build and install the package.
The sdist is a bundle of your source code, any data files, and crucially, the metadata and build instructions needed to construct a wheel (the preferred binary distribution format) or install directly. When pip receives an sdist, it first checks if it can build a wheel from it. If it can, it will build and install the wheel. If not, or if explicitly told to build from source, it will execute the build process defined in your setup.py or pyproject.toml. This involves compiling any C extensions (if you have them), packaging data files, and then installing the result.
The core of building an sdist lies in setup.py (or pyproject.toml with a build backend like setuptools). The setuptools.setup() function is where you declare your package’s name, version, entry points, dependencies, and how to find your package’s modules. find_packages() is a convenient helper that automatically discovers all packages (directories containing an __init__.py) within your project. For more complex projects, you might list packages explicitly or use package_dir and package_data arguments to control which files are included.
What most people don’t realize is that setuptools is highly configurable and can handle much more than just Python source files. You can instruct it to include data files, compile C/C++/Cython extensions, and even define custom build steps. The setup() function’s arguments are extensive, controlling everything from the license and classifiers (which help categorize your package on PyPI) to the inclusion of non-Python files. For instance, if you had a README.md you wanted to be displayed on PyPI and included in the sdist, you’d typically add long_description=open('README.md').read() and long_description_content_type='text/markdown' to your setup() call.
When you distribute an sdist, you’re essentially providing a recipe for others to build your package. This is incredibly powerful because it allows for flexibility across different operating systems and Python environments, but it also means your build process needs to be robust enough to work for your users.
The next hurdle after mastering sdists is understanding how to create and leverage wheels, which are pre-built distributions that offer faster installation times for users.