Packaging Basics
9 examples to get you started with Packaging and Publishing - 6 basic and 3 intermediate.
Prerequisites
- Python 3.14.0 with
uv 0.6+orpip - A project with
pyproject.tomlandsrc/layout - PyPI account (or TestPyPI) for publishing examples
Basic Examples
1. Build a Wheel
A wheel (.whl) is the standard binary distribution format.
uv build
ls dist/
# myapp-0.1.0-py3-none-any.whl
# myapp-0.1.0.tar.gzuv buildcreates wheel and sdist indist/py3-none-anymeans pure Python, any platform- Wheels install faster than sdists (no build step)
- Never commit
dist/- build in CI
Related: Build Backends - hatchling vs setuptools
2. pyproject.toml for Packaging
Package metadata lives in [project].
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = ["httpx>=0.28"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"nameis the PyPI package name (lowercase, hyphens)versionfollows SemVer[build-system]declares the build backend- Import name may differ from package name (
my_appvsmy-app)
Related: pyproject.toml Explained
3. src Layout
Keep source in src/ for reliable packaging and testing.
myapp/
├── pyproject.toml
├── src/myapp/
│ ├── __init__.py
│ └── core.py
└── tests/
src/prevents accidental imports from repo root- Build backend discovers packages from config
- Tests import the installed package
- Industry standard for new projects
4. Install from Local Build
Test your package before publishing.
uv build
uv pip install dist/myapp-0.1.0-py3-none-any.whl
python -c "import myapp; print(myapp.__version__)"- Install the wheel to verify packaging is correct
- Catches missing files and import errors
- Test in a fresh venv for accuracy
- Also test
pip install .(sdist path)
5. Version Your Package
Expose version in __init__.py or use dynamic versioning.
[project]
version = "0.1.0"# src/myapp/__init__.py
__version__ = "0.1.0"- Single source of truth in
pyproject.tomlpreferred - Dynamic versioning from git tags with hatchling
- SemVer: MAJOR.MINOR.PATCH
- Pre-release:
0.2.0b1
Related: Versioning & Changelogs
6. Console Script Entry Point
Ship a CLI command with your package.
[project.scripts]
myapp = "myapp.cli:main"# src/myapp/cli.py
def main() -> None:
print("Hello from myapp CLI")- Installed to
.venv/bin/myapponpip install - Uses
project.scriptsin pyproject.toml - Function takes no arguments (uses argparse/click internally)
- Test with
uv pip install -e .thenmyapp
Related: Entry Points & Console Scripts
Intermediate Examples
7. Publish to TestPyPI
Dry-run publishing before production PyPI.
uv build
uv publish --index testpypi
# Test install:
uv pip install --index-url https://test.pypi.org/simple/ myapp- TestPyPI is a sandbox registry
- Verify metadata, install, and CLI work
- Use trusted publishing (OIDC) in CI, not passwords
- Promote to PyPI after TestPyPI verification
Related: Publishing to PyPI
8. Include Package Data
Ship non-Python files (templates, config defaults).
[tool.hatch.build.targets.wheel]
packages = ["src/myapp"]
[tool.hatch.build.targets.wheel.force-include]
"src/myapp/templates" = "myapp/templates"- Templates, JSON schemas, and static assets need explicit inclusion
force-includemaps source to install path- Use
importlib.resourcesto access at runtime - Verify with
pip installand inspect site-packages
9. Build in CI
Automate wheel building on every release tag.
- run: uv build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/- Build artifacts attached to GitHub releases
- Same wheel published to PyPI from CI
- GPG signing optional for high-security packages
- cibuildwheel for multi-platform native extensions
Related: Packaging Best Practices
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.