Phase 6 · Applied & ProfessionalModule 39~36 min read

Packaging, Distribution & Deployment

Turn your project into a shareable, installable package.

What you'll learn

A script on your laptop helps only you. Packaging turns it into something anyone can pip install and run — with a clear structure, declared dependencies, a command-line entry point, and, when needed, a container for deployment.

By the end of this lesson you'll be able to:

  • Lay out a project with the recommended src structure
  • Declare metadata and dependencies in pyproject.toml
  • Build wheels and source distributions and publish to PyPI
  • Expose your program as a command with a console entry point
  • Containerize an app with a simple Dockerfile

Project structure

A predictable layout makes a project easy to build, test, and understand. The modern convention is the src layout: your importable package lives under src/, with tests and config alongside.

project tree
myapp/
├── src/
│   └── myapp/
│       ├── __init__.py
│       └── cli.py
├── tests/
│   └── test_cli.py
├── pyproject.toml       # project metadata & dependencies
└── README.md

Note

The src/ layout prevents a subtle bug: it stops Python from accidentally importing your package from the working directory instead of the installed version, so your tests exercise what users will actually get.

pyproject.toml

pyproject.toml is the single, standard config file for a Python project. It declares your metadata, dependencies, entry points, and which build backend to use.

pyproject.toml
[project]
name = "myapp"
version = "0.1.0"
description = "A friendly greeter."
requires-python = ">=3.10"
dependencies = ["requests>=2.31"]

[project.scripts]
myapp = "myapp.cli:main"     # the 'myapp' command runs cli.main()

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Building & publishing

Building produces two artifacts: a wheel (.whl, a fast, pre-built install) and an sdist (.tar.gz, the source). twine uploads them to PyPI, where pip install myapp can find them.

terminal
$ pip install build twine
$ python -m build          # creates dist/*.whl and dist/*.tar.gz
$ twine upload dist/*      # publish to PyPI (test on TestPyPI first)

Tip

Follow semantic versioning — MAJOR.MINOR.PATCH — and bump the version every release. Practice publishing on TestPyPI first; a version number on real PyPI can never be reused.

Console entry points

The [project.scripts] table turns a function into a terminal command. After install, running myapp calls myapp.cli:main — no python -m, no path needed.

cli.py
# src/myapp/cli.py
import argparse

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("name")
    args = parser.parse_args()
    print(f"Hello, {args.name}!")

if __name__ == "__main__":
    main()
terminal
$ pip install .
$ myapp Ada          # the entry point makes this a real command
Hello, Ada!

Docker & deployment

A container bundles your app and its exact environment so it runs identically everywhere — your machine, a teammate's, or a server. A minimal Dockerfile installs your package and sets the command to run:

Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir .
CMD ["myapp", "world"]

Key idea

Packaging and containers solve the same problem at different scales: "it works on my machine" becomes "it works anywhere." Declare dependencies explicitly, pin them for reproducibility, and never bake secrets into the image — pass them in as environment variables at runtime.

Recap & quick check

Key takeaways

  • Use the src layout: your package under src/, with tests/ and pyproject.toml alongside.
  • pyproject.toml is the standard config: metadata, dependencies, entry points, and build backend.
  • Building creates a wheel (.whl, prebuilt) and an sdist (.tar.gz); twine uploads them to PyPI.
  • Follow semantic versioning and test on TestPyPI first — a PyPI version number can't be reused.
  • [project.scripts] exposes a function as a terminal command (console entry point).
  • A Dockerfile bundles app + environment for identical runs anywhere; pass secrets via env vars, never bake them in.

Quick check

1. What is pyproject.toml for?

2. What's the difference between a wheel and an sdist?

3. What does [project.scripts] with 'myapp = "myapp.cli:main"' do?

4. Why practice publishing on TestPyPI first?

5. What problem do containers primarily solve?

You can ship code now. The last professional skills tie it together: writing clean code and collaborating with Git. Next up: Module 40 — Clean Code, Git & Professional Workflow.