Skip to content

Packaging & Build Systems

A build system transforms high-level source code into machine-executable binary artifacts. These artifacts can then be indexed and distributed as modular components for larger projects. In the case of Python, these artifacts are called "wheels".

There are many benefits in this approach to software delivery, however, it is a broad topic and the above is not the complete truth ("source" distributions are commonly available).

Integration with Private Package Indices

We can point uv to a private index through relevant config blocks in pyproject.toml:

[[tool.uv.index]]
name = "some-alias"
url = "https://${INDEX_HOSTNAME}/${INDEX_PYPI_SIMPLE_PATH}"
publish-url = "https://${INDEX_HOSTNAME}/${INDEX_PYPI_PATH}"
explicit = true

Dependency Groups

Dependencies can be separated by "group" i.e. include documentation generators like zensical under a docs group and test frameworks such as pytest under a test group:

[project]
name = "example"
version = "0.1.0"
requires-python = ">=3.11"

[tool.uv]
default-groups = ["dev", "docs", "test"]

[dependency-groups]
dev = [
    "twine >= 6.2.0, < 7",
]
docs = [
    "markdown-callouts >= 0.4.0, < 2",
    "markdown-exec[ansi] >= 1.12.1, < 2",
    "mkdocstrings[python] >= 0.30.1, < 2",
    "zensical >= 0.0.30, < 2",
]
test = [
    "coverage >= 7.3.3, < 8",
    "hypothesis >= 6.138.6, < 7",
    "mutmut >= 3.2.3, < 4",
    "pytest >= 9.0.2, < 10",
    "pytest-asyncio >= 0.26.0, < 2",
    "pytest-env >= 1.1.5, < 2",
    "pytest-html >= 4.1.1, < 5",
    "pytest-randomly >= 3.16, < 4",
    "tox >= 4.25, < 5",
    "tox-uv >= 1.33.4, < 2",
]

Tip

Modern build systems efficiently resolve the "graph" of package dependencies helping to prevent "dependency hell".

Test Environments

You can control pytest and its test environment through relevant config blocks in pyproject.toml:

[tool.pytest.ini_options]
log_cli = true
log_cli_level = "INFO"
env = [
    'ENV_VAR_1=mock-value-1',
    'ENV_VAR_2=mock-value-2',
    # ...
    'ENV_VAR_N=mock-value-n',
]

[tool.coverage.run]
source = ["src"]
branch = true
omit = ["tests/**/*"]

[tool.coverage.report]
omit = ["tests/**/*"]

Idempotence

Each package build is made reproducible with any dependencies being "locked" to specific versions in a "lockfile".

The "lockfile" can then be used to rebuild a stable virtual environment with the exact same dependency versions.

Below is an example of a uv.lock file (the "lock file" format used by uv):

# This file is automatically @generated by `uv` and should not be changed by hand.

version = 1
requires-python = ">=3.12"
resolution-markers = [
    "python_full_version < '3.13'",
    "python_full_version >= '3.13'",
]

[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]

[[package]]
name = "anyio"
version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "idna" },
    { name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", size = 163930 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", size = 86780 },
]

# ...

The lockfile is automatically updated upon any change to package dependencies.

Implications for CI/CD

Tip

The use of a modern "build system" such as uv underpins CI/CD.

This is because the "CI" pipeline can be reduced to a sequence of commands against the build system.

Stage Command
Format uv run black
Lint uv run ruff
Test uv run pytest
Coverage (Reporting) uv run coverage run -m pytest
Coverage (HTML) uv run coverage html
Build uv build
Publish uv publish

Alternative Build Systems

This template uses the uv build system.

As of writing, uv is widely regarded to be state of the art for Python.