Skip to main content
Python for the JVM Engineer

Packaging for Distribution

Ravinder··5 min read
PythonJVMJavapackagingwheelPyPIdistributionpyinstaller
Share:
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

flowchart LR subgraph "Java" J1["library.jar\n(reusable library)"] J2["app-all.jar\n(uber-JAR, Spring Boot)"] J3["JRE + app\n(jlink image)"] end subgraph "Python" P1["library-1.0-py3-none-any.whl\n(wheel, reusable library)"] P2["app binary\n(PyInstaller / Nuitka)"] P3["Docker image\n(runtime + app)"] end J1 -.->|analogous| P1 J2 -.->|analogous| P2 J3 -.->|analogous| P3

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 platform
  • numpy-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.whl

project.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.py

This 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 Python

Tradeoffs 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.py

Nuitka 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.0

Tag and release workflow:

sequenceDiagram participant Dev participant Git participant CI participant PyPI Dev->>Git: git tag v1.2.0 Git->>CI: tag push triggers workflow CI->>CI: python -m build CI->>CI: twine check dist/* CI->>PyPI: twine upload dist/* PyPI-->>Dev: package published

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 build produces both a wheel and an sdist; publish both with twine upload or uv 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.scripts in pyproject.toml defines console entry points — equivalent to specifying Main-Class in a JAR manifest.
  • The entry point group system (importlib.metadata.entry_points) is Python's ServiceLoader — use it for plugin-based extensibility.
Share: