You can upload your Python packages to PyPI using pip and twine.
Here’s a package structure that works well:
my_package/
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── module1.py
├── pyproject.toml
├── README.md
└── LICENSE
The pyproject.toml file is crucial. It tells build tools like setuptools and twine how to build and package your project. Here’s a minimal example:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "0.1.0"
authors = [
{ name="Your Name", email="your.email@example.com" },
]
description = "A short description of my package"
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.urls]
"Homepage" = "https://github.com/yourusername/my_package"
"Bug Tracker" = "https://github.com/yourusername/my_package/issues"
To build your package, you’ll need build. Install it:
pip install build
Then, from the root of your project (where pyproject.toml is), run:
python -m build
This command creates a dist/ directory containing two files: a source distribution (.tar.gz) and a wheel (.whl).
Now, for uploading. You need twine. Install it:
pip install twine
Before uploading to the real PyPI, it’s highly recommended to test on TestPyPI. You’ll need an account there. Create one at https://test.pypi.org/account/register/.
To upload to TestPyPI, use twine upload:
twine upload --repository testpypi dist/*
twine will prompt you for your TestPyPI username and password. If you have 2-factor authentication enabled on your PyPI account, you’ll need to generate an API token from your PyPI account settings and use that token as your password.
Once you’ve successfully uploaded to TestPyPI and verified it, you can upload to the real PyPI. Create an account at https://pypi.org/account/register/ if you don’t have one.
To upload to the real PyPI:
twine upload dist/*
Again, you’ll be prompted for your username and password (or API token).
After a successful upload, your package should be available on PyPI within a few minutes. You can then install it using pip:
pip install my-package
The most surprising thing about package distribution is how much of it is not about the code itself, but about metadata and build system configuration. Your pyproject.toml is the control panel for your package’s identity and how it’s assembled, not just a list of dependencies.
Here’s a small Python script demonstrating how to programmatically check for installed packages and their versions, which is analogous to what pip does when you install or check dependencies:
import pkg_resources
def check_package_installed(package_name):
try:
version = pkg_resources.get_distribution(package_name).version
print(f"Package '{package_name}' is installed with version: {version}")
return True
except pkg_resources.DistributionNotFound:
print(f"Package '{package_name}' is not installed.")
return False
# Example usage:
check_package_installed("twine")
check_package_installed("nonexistent-package-xyz")
When you run this, pkg_resources (part of setuptools) queries the environment to find installed distributions. twine uses a similar mechanism internally when resolving dependencies or checking compatibility, but its primary role is the upload process itself, interacting with the PyPI API.
The dist/* glob pattern in the twine upload command is a simple but powerful wildcard that tells twine to upload all files within the dist/ directory. This usually includes both the source distribution (.tar.gz) and the built wheel (.whl) files, ensuring maximum compatibility for users installing your package.
The next step after publishing is often managing package versions and handling deprecations, which involves careful planning of your versioning scheme and communication with users.