How do you package and deploy a Python application?
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
| Workload | Common choice |
|---|---|
| Long-running web service, need fine control | Kubernetes / ECS running the container |
| Simpler apps, less ops overhead desired | PaaS (Heroku, Fly.io, Render) — often deploys straight from a Procfile/buildpack, no Dockerfile needed |
| Event-driven, sporadic/bursty invocations | Serverless (AWS Lambda, Google Cloud Functions) — packaged as a zip/layer or container image, billed per invocation |
| CLI tool distributed to end users | A 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.