PyPI is the distribution channel behind much of the Python ecosystem. For an Indian developer, research team, startup, or open-source maintainer, publishing a package can turn an internal utility into a reusable product—but only if the package is easy to install, well documented, tested, and maintained.
This guide shows a modern workflow for creating a Python PyPI package in 2026. It uses pyproject.toml, isolated environments, standard build tools, TestPyPI, and trusted publishing rather than relying on the older setup.py-first approach.
What PyPI does—and does not do
The Python Package Index is a public registry for Python distribution files and their metadata. Users typically install packages with pip, while maintainers upload built artifacts with tools such as twine or a CI platform configured for trusted publishing.
PyPI provides:
- Package discovery and version history.
- Distribution of source archives and wheels.
- Dependency metadata used by installers.
- Project pages containing documentation from your README.
- Namespace ownership and release management.
PyPI is not a substitute for source control, documentation hosting, security review, or support operations. Keep your source code in a repository such as GitHub or GitLab, and link it from the package metadata.
A reusable package is especially valuable when it supports work such as Python scripts for automating data preprocessing, where consistent installation and dependency handling matter across notebooks, servers, and CI environments.
Choose a package boundary before writing code
Start with one clear problem. A package that converts Indian address data, wraps an internal model endpoint, validates API responses, or provides a reusable ML utility is easier to explain than a collection of unrelated helpers.
Define:
- Public API: Functions, classes, and command-line commands users should rely on.
- Supported Python versions: Test only versions you intend to support.
- Dependencies: Separate runtime, optional, development, and documentation dependencies.
- License: Choose a license that matches how others may use your code.
- Compatibility promise: State what counts as a breaking change.
If the package will support AI applications, keep provider-specific integrations behind stable interfaces. A focused library can then serve projects involving LLM APIs in Python web apps without forcing every user to adopt the same web framework or deployment model.
Use a modern project layout
A src layout prevents accidental imports from the repository root and catches packaging mistakes earlier. A practical structure is:
my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── core.py
├── tests/
│ └── test_core.py
└── .github/workflows/
└── publish.ymlThe distribution name on PyPI may contain hyphens, while the import name normally uses underscores. For example, users might install my-package and write import my_package.
Configure metadata in pyproject.toml
Modern Python packaging tools read project metadata from pyproject.toml. Hatchling, Setuptools, and PDM are all viable build backends; choose one and follow its documentation consistently. The following example uses Hatchling:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-package"
version = "0.1.0"
description = "A concise description of the package"
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
authors = [{ name = "Your Name", email = "you@example.com" }]
dependencies = [
"httpx>=0.27,<1.0"
]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy", "build"]
[project.urls]
Homepage = "https://github.com/example/my-package"
Issues = "https://github.com/example/my-package/issues"Avoid placing secrets, tokens, private URLs, or environment-specific configuration in this file. Keep dependencies bounded thoughtfully: overly loose ranges can introduce surprise breakage, while overly strict pins make reuse difficult.
Write a small, stable public API
Keep internal implementation details private and expose only what users need. For example:
# src/my_package/core.py
def greet(name: str) -> str:
"""Return a greeting for a non-empty name."""
if not name.strip():
raise ValueError("name must not be empty")
return f"Hello, {name}!"Use type hints, docstrings, predictable exceptions, and deterministic behaviour. If your package handles data or model outputs, document encoding, timezone, missing-value, and retry assumptions explicitly. These details matter more than a long feature list when the package enters a production end-to-end ML pipeline in Python.
Test the package as an installed user would
Install development dependencies in an isolated environment:
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
pytestTest behaviour, not private implementation details. Include cases for invalid input, empty values, network failures, and supported Python versions. Run format and quality checks as well:
ruff check .
ruff format --check .
mypy srcBefore building, verify that the package works from a clean environment rather than only through an editable install. This catches missing package files and undeclared dependencies.
Build and inspect distribution files
Install the build frontend and create both a wheel and source archive:
python -m pip install build
python -m buildThe dist/ directory should contain files similar to:
my_package-0.1.0-py3-none-any.whl
my_package-0.1.0.tar.gzInspect the artifacts before uploading:
python -m pip install twine
python -m twine check dist/*For packages with compiled extensions, platform-specific wheels, or large AI dependencies, test the built wheel separately. A package that works from source may fail when installed from the artifact users actually download.
Use TestPyPI before the real index
TestPyPI is a separate registry for release validation. Create an account there, upload a unique version, and install it into a fresh environment:
python -m twine upload --repository testpypi dist/*
python -m pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ my-package==0.1.0The extra index is useful when your package depends on public packages that are not mirrored on TestPyPI. Check the README rendering, metadata, imports, command-line entry points, and dependency resolution.
Publish securely to PyPI
Create the project on PyPI through the first upload, or configure the project after it exists. For manual releases, use an API token rather than a password:
python -m twine upload dist/*For a serious project, prefer trusted publishing from GitHub Actions or another supported CI provider. It avoids storing a long-lived PyPI token in repository secrets and can release only from a protected tag or approved workflow. Restrict who can create release tags, require review for workflow changes, and keep build logs free of credentials.
Never upload .env files, private keys, customer data, model weights with unclear rights, or generated files containing secrets. Review the archive contents before every release.
Versioning and maintenance
PyPI does not allow replacing a file uploaded under the same version. To correct a release, increment the version and publish a new artifact. Follow semantic versioning where it fits:
- Patch releases fix backwards-compatible defects.
- Minor releases add backwards-compatible features.
- Major releases may change the public API.
Maintain a changelog, deprecation notices, supported Python versions, and migration examples. Pin your own production applications with lockfiles, but avoid unnecessarily pinning every dependency in the published library metadata.
Monitor dependency vulnerabilities, rotate credentials, respond to issue reports, and test against new Python releases before claiming support. If a package becomes part of a startup's data or AI stack, document operational limits such as rate limits, latency, licensing, and data residency.
A release checklist
Before publishing a version, confirm that:
- The package name is available and not confusingly similar to another project.
- Metadata, license, README, homepage, and supported Python versions are correct.
- Tests pass in clean environments.
- The wheel and source archive install successfully.
twine checkreports no metadata or rendering errors.- Secrets and unwanted files are absent from
dist/. - The version has changed since the previous release.
- The release is tested on TestPyPI or an equivalent staging process.
- CI permissions and publishing rules are restricted.
A well-built Python PyPI package is more than an uploaded ZIP file. It is a maintained contract between your code and its users—one that should make installation predictable, upgrades understandable, and contribution straightforward. That discipline also improves technical portfolios, including Python developer portfolio projects for university students, because reviewers can inspect a real release workflow instead of a code dump.
FAQ
Can I publish a private package on public PyPI?
No. Anything on PyPI is publicly accessible. Use a private package index or repository service for proprietary code.
Do I still need `setup.py`?
Usually not for project configuration. Modern projects can define metadata in pyproject.toml; some tools may still generate or use compatibility files.
Should I upload a wheel, source archive, or both?
Upload both when possible. Wheels install faster, while source archives provide a portable fallback and are important for some build workflows.
Can I delete a published release?
PyPI has limited deletion semantics and cached downloads may persist. Treat every release as public and immutable; publish a corrected version instead.
Apply for AI Grants India
If you are building an India-focused AI library, developer tool, or open-source infrastructure project, explore support through AI Grants India.