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 — 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).
Interview-ready summary: 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.