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
srcstructure - 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.
myapp/
├── src/
│ └── myapp/
│ ├── __init__.py
│ └── cli.py
├── tests/
│ └── test_cli.py
├── pyproject.toml # project metadata & dependencies
└── README.mdNote
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.
[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.
$ 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
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.
# 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()$ 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:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir .
CMD ["myapp", "world"]Key idea
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.