Packaging for Distribution
Distributing a Java application is well-understood: build a JAR (library) or an uber-JAR / application JAR with an embedded server (Spring Boot's executable JAR), publish to Maven Central or Nexus, or package a Docker image with a slim JRE. Python's distribution formats map to these concepts directly — once you know which Python format answers which Java question.
The Format Map
Wheels — The Standard Library Format
A wheel (.whl) is a ZIP archive with a specific naming convention. It is the install-ready format for Python libraries — the equivalent of a JAR on Maven Central.
Wheel naming: {name}-{version}-{python}-{abi}-{platform}.whl
Examples:
requests-2.31.0-py3-none-any.whl— pure Python, any platformnumpy-1.26.0-cp311-cp311-manylinux_2_17_x86_64.whl— CPython 3.11, Linux x86-64, compiled C
The manylinux tag is the Python equivalent of a fat JAR containing native libraries for multiple Linux distributions — it ensures the wheel installs on a wide range of Linux systems without recompilation.
Building a Wheel
# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-library"
version = "1.2.0"
requires-python = ">=3.10"
dependencies = ["requests>=2.28"]
[project.scripts]
my-cli = "my_library.cli:main" # entry point → console script# Build wheel and sdist
python -m build # produces dist/*.whl and dist/*.tar.gz
# Inspect contents
unzip -l dist/my_library-1.2.0-py3-none-any.whlproject.scripts is the Python equivalent of Maven's <mainClass> in the JAR manifest — it generates a console script entry point that callers can run by name after installation.
Publishing to PyPI
pip install twine
twine upload dist/*
# Or with uv
uv publish dist/*Publishing to a private index (Nexus/Artifactory equivalent):
twine upload --repository-url https://my-nexus.example.com/repository/pypi/ dist/*Configure in ~/.pypirc or via environment variables (TWINE_REPOSITORY_URL, TWINE_USERNAME, TWINE_PASSWORD).
Standalone Executables — The Uber-JAR Equivalent
When you want to ship a CLI tool or desktop application without requiring Python to be installed, use PyInstaller or Nuitka.
PyInstaller
pip install pyinstaller
pyinstaller --onefile --name my-tool src/my_library/cli.pyThis bundles the Python interpreter, all dependencies, and the application code into a single executable (dist/my-tool). Like a Spring Boot executable JAR — self-contained, no external runtime needed.
# cli.py
import click
@click.command()
@click.option("--name", default="World")
def main(name: str) -> None:
print(f"Hello, {name}!")
if __name__ == "__main__":
main()pyinstaller --onefile --name greet cli.py
./dist/greet --name Alice # works on a machine with no PythonTradeoffs versus Spring Boot JAR:
- The executable embeds a CPython runtime (50–150 MB uncompressed)
- Startup time is 0.5–2 seconds (extracting the bundled files)
- Cross-compilation is not supported — build on the target platform
Nuitka — The Compiled Option
Nuitka compiles Python to C and then to a native binary — closer to GraalVM native image than PyInstaller:
pip install nuitka
python -m nuitka --standalone --onefile --output-dir=dist cli.pyNuitka produces a faster executable (native code, not interpreter overhead) at the cost of longer build times. The analogy to GraalVM native image is close: longer compilation, faster startup, smaller memory footprint.
Source Distributions (sdist)
The tar.gz produced by python -m build is the source distribution — the Python equivalent of publishing source JARs. PyPI hosting both a wheel and an sdist is the norm:
ls dist/
# my_library-1.2.0-py3-none-any.whl ← wheel (preferred)
# my_library-1.2.0.tar.gz ← sdist (fallback / source)Version Management
Use semantic versioning. Automate version bumps with bump2version or hatch version:
hatch version patch # 1.2.0 → 1.2.1
hatch version minor # 1.2.1 → 1.3.0
hatch version major # 1.3.0 → 2.0.0Tag and release workflow:
This mirrors Maven's release:perform / GitHub Actions → Maven Central workflow.
Entry Points and Plugins
Python's entry point system enables plugin architectures — the equivalent of Java's ServiceLoader:
[project.entry-points."myapp.plugins"]
csv-exporter = "my_library.exporters.csv:CsvExporter"
json-exporter = "my_library.exporters.json:JsonExporter"# At runtime, discover plugins
from importlib.metadata import entry_points
for ep in entry_points(group="myapp.plugins"):
plugin_class = ep.load()
plugin_class().export(data)Third-party packages can add themselves to your plugin group by declaring the same entry-points group name — no code changes to the host application.
Key Takeaways
- A wheel (
.whl) is the Python equivalent of a JAR — it is the install-ready format published to PyPI or a private index (Nexus/Artifactory). python -m buildproduces both a wheel and an sdist; publish both withtwine uploadoruv publish.- PyInstaller bundles the CPython runtime into a single executable — the Spring Boot executable JAR analogue for CLI tools.
- Nuitka compiles Python to native code (GraalVM native image analogue) for faster startup and lower memory, at the cost of build time.
project.scriptsinpyproject.tomldefines console entry points — equivalent to specifyingMain-Classin a JAR manifest.- The entry point group system (
importlib.metadata.entry_points) is Python'sServiceLoader— use it for plugin-based extensibility.