How do you package and deploy a Python application?

7 minintermediatedeploymentdockerpackagingproduction

Quick Answer

For a library, build a wheel (python -m build) and publish it to PyPI (or a private index) so it can be installed via pip. For a deployable application/service, the standard modern approach is a Docker container with dependencies pinned via a lock file, giving a reproducible runtime environment independent of the host machine's Python version or installed packages. It's orchestrated via Kubernetes, a PaaS (Heroku, Fly.io), or a serverless platform depending on the workload.

Detailed Answer

Packaging a library: wheels and PyPI

python -m build              # builds dist/mypackage-1.0.0-py3-none-any.whl + a source dist
python -m twine upload dist/*   # publishes to PyPI

A wheel (.whl) is a pre-built, ready-to-install package format. pip install mypackage fetches and installs it without needing to run any build step on the user's machine (unlike a source distribution, which may require compiling C extensions locally). This is the right target when the deliverable is a reusable library other projects will pip install.

Packaging an application: containerization

FROM python:3.12-slim

WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install poetry && poetry install --no-dev --no-root

COPY . .
CMD ["python", "-m", "myapp"]

For a deployable service (as opposed to a library), a Docker image bundles the exact Python version, exact pinned dependencies (via the lock file), and the application code into one artifact that runs identically regardless of the host machine, eliminating "works on my machine" class issues entirely, since the container is the runtime environment.

Why containers dominate for applications specifically

  • Reproducibility: the same image runs identically in CI, staging, and production — no drift from differing host Python versions or system libraries.
  • Isolation: no conflicts with other applications' dependencies on the same host.
  • Orchestration compatibility: container images are the standard unit Kubernetes, ECS, and most modern deployment platforms expect.

Deployment targets, by workload shape

WorkloadCommon choice
Long-running web service, need fine controlKubernetes / ECS running the container
Simpler apps, less ops overhead desiredPaaS (Heroku, Fly.io, Render) — often deploys straight from a Procfile/buildpack, no Dockerfile needed
Event-driven, sporadic/bursty invocationsServerless (AWS Lambda, Google Cloud Functions) — packaged as a zip/layer or container image, billed per invocation
CLI tool distributed to end usersA wheel published to PyPI, or a bundled executable (pyinstaller, shiv) for non-Python-savvy users

Entry points and process management in production

gunicorn myapp.wsgi:application --workers 4     # WSGI, process-based concurrency
uvicorn myapp.asgi:application --workers 4        # ASGI, event-loop-based concurrency per worker

A production WSGI/ASGI application is served by a dedicated application server (Gunicorn, Uvicorn) rather than a development server (Django's runserver, Flask's built-in dev server). The dev servers are explicitly not designed for production traffic — no proper worker management, no production-grade concurrency handling.

Libraries are packaged as wheels and published to PyPI for pip install. Deployable applications are typically containerized with pinned dependencies for full environment reproducibility, then run via an orchestration platform or PaaS matched to the workload's shape (long-running service, serverless, or CLI tool). Production traffic is always served by a dedicated app server (Gunicorn/ Uvicorn), never a framework's built-in development server.