Packaging — pyproject.toml and Wheels¶
Overview¶
Turn an ops tool into an installable package with pyproject.toml, a console script entry point, a built wheel, and clear versioning/dependency metadata.
PYTHONPATH=. works in labs. Teams need pip install (or uv) of an internal wheel/index. Modern packaging centres on pyproject.toml + build backends (hatchling/setuptools).
Complete Testing with pytest first. Diagrams use Excalidraw only.
This is a core tutorial in Module 23 · Packaging of the REBASH Academy Python for Cloud & DevOps Engineers series — written for Cloud, DevOps, Platform, and SRE engineers.
Prerequisites¶
Required¶
Learning Objectives¶
By the end of this tutorial, you will be able to:
- Author a minimal
pyproject.toml - Declare
requires-pythonand dependencies - Define a console script entry point
- Build an sdist/wheel
- Explain versioning and private index publishing
- Install the package into a clean venv
Architecture¶
This topic’s control points and relationships are shown below.
Theory¶
What it is¶
Packaging turns a folder of scripts into an installable distribution: metadata, dependencies, and entry-point console scripts. Modern Python projects declare that in pyproject.toml (PEP 621). A source distribution (sdist) ships source; a wheel is a built install artefact that pip/uv can install quickly without running arbitrary setup code at install time (for pure Python).
Why it matters¶
DevOps tools leave the lab when colleagues run pip install rebash-healthcheck (or install a private wheel in CI images). Without packaging you copy files and hope PYTHONPATH is set. With packaging you get versioned releases, dependency declarations, and a stable CLI name via [project.scripts]. Internal indexes (Artifactory, CodeArtifact, GitHub Packages) become the team’s “app store” for automation.
How it works¶
[project] holds name, version, requires-python, and dependencies. [project.scripts] maps a command to module:function. [build-system] names the build backend (often setuptools or hatchling). Build with python -m build (or uv build) to produce dist/*.whl and an sdist. Install the wheel into a venv to verify the console script. Publish on git tags: public to PyPI, private to your index URL documented in the README and CI.
[project]
name = "rebash-healthcheck"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
rebash-healthcheck = "rebash_healthcheck.cli:main"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
Key concepts and comparisons¶
| Artefact | Contents | Use |
|---|---|---|
| sdist | Source + metadata | Rebuild on exotic platforms |
| wheel | Built package | Fast, preferred installs |
| Dependency style | When |
|---|---|
Ranges (>=x,<y) | Libraries others import |
| Exact pins / lockfile | Apps and deploy images |
| Optional extras | Heavy optional features |
Version with SemVer-style MAJOR.MINOR.PATCH. Automate from git later (setuptools_scm) once the basics are solid.
Common pitfalls¶
- Forgetting
[project.scripts]so the CLI never appears onPATH. - Shipping secrets or huge fixtures inside the package data.
- Publishing
0.0.0repeatedly without tags — consumers cannot pin. - Mixing editable installs and system Python until imports resolve wrongly.
- Using an untrusted private index without Transport Layer Security (TLS) verification.
Hands-on Lab¶
Focus: practise the core workflow for Packaging — pyproject.toml and Wheels
mkdir -p ~/rebash-python/module-23
cd ~/rebash-python/module-23
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install 'build==1.2.2.post1'
Step 1 – Package layout¶
cd ~/rebash-python/module-23
source .venv/bin/activate
mkdir -p src/rebash_healthcheck
cat > pyproject.toml << 'EOF'
[project]
name = "rebash-healthcheck"
version = "0.1.0"
description = "Module 23 lab CLI"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
rebash-healthcheck = "rebash_healthcheck.cli:main"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
EOF
cat > README.md << 'EOF'
# rebash-healthcheck
Lab package for Module 23.
EOF
cat > src/rebash_healthcheck/__init__.py << 'EOF'
__version__ = "0.1.0"
EOF
cat > src/rebash_healthcheck/cli.py << 'EOF'
from __future__ import annotations
import argparse
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(prog="rebash-healthcheck")
p.add_argument("--version", action="store_true")
args = p.parse_args(argv)
if args.version:
from rebash_healthcheck import __version__
print(__version__)
return 0
print("ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())
EOF
Step 2 – Build¶
Step 3 – Install into a clean venv¶
python3 -m venv /tmp/rebash-pkg-test
/tmp/rebash-pkg-test/bin/pip install dist/rebash_healthcheck-0.1.0-py3-none-any.whl
/tmp/rebash-pkg-test/bin/rebash-healthcheck --version
/tmp/rebash-pkg-test/bin/rebash-healthcheck
Step 4 – Editable install for development¶
Validation¶
- Lab commands run under
~/rebash-python/module-23/ - You can explain each Theory section in your own words
- You used modern tooling where it applies to this topic
- You can describe one production failure mode for this topic
Code Walkthrough¶
Production practice for Packaging — pyproject.toml and Wheels always combines:
- Inspect before you change (status, plan, logs, dry-run)
- Prefer reversible, documented changes (Git, IaC, drop-ins, version pins)
- Capture evidence (command output, pipeline logs) for handovers
- Prefer current tools and APIs over legacy shortcuts
- Least privilege — escalate credentials only when required
Keep runbooks short enough to follow under pressure. Automate checks; keep humans for judgement.
Security Considerations¶
- Treat credentials and tokens for python as privileged — never commit them
- Prefer short-lived auth (OIDC, roles, SSO) over long-lived keys
- Validate blast radius before apply/deploy/delete operations
- Restrict who can approve production changes
- Collect audit logs; limit who can read sensitive traces
Common Mistakes¶
Forgetting [project.scripts] so the CLI never appears on PATH.
Validate assumptions against the Theory section and official docs before changing production.
Shipping secrets or huge fixtures inside the package data.
Lab shortcuts (open security groups, admin roles, skip approvals) must not ship unchanged.
Changing production without a rollback path
Always know how to revert (previous artefact, prior release, state rollback, DNS failback).
Best Practices¶
- Encode Packaging — pyproject.toml and Wheels changes as code and review them in pull requests
- Pin versions (images, modules, actions, provider plugins)
- Separate environments with clear promotion gates
- Alert on symptoms with runbooks attached
- Destroy lab resources; tag everything with owner and expiry where possible
Troubleshooting¶
| Symptom | Likely cause | What to do |
|---|---|---|
| Package not found | Wrong where / src layout | Fix setuptools package find |
| Script missing | Entry point typo | Check module:function |
| Wrong Python | requires-python | Build/install with 3.12+ |
Summary¶
pyproject.tomlis the packaging source of truth- Wheels install cleanly in CI and servers
- Console scripts expose CLIs
- Publish to a private index for teams
Interview Questions¶
- How does Packaging — pyproject.toml and Wheels show up when operating Cloud or production platforms?
- What would you check first if this area misbehaves in production?
- Which modern tools or APIs replace older equivalents here?
- What security control should accompany this capability?
- How would you automate verification of this topic in CI?
Sample answer — question 2
Start with blast radius and recent changes, gather evidence (logs, status, plan/diff), then fix forward with a known rollback path — not guesswork.