BUILD(1)

πŸ“¦
version 1.5.1 Β· 5 versions
, in ubuntu stonking

✨ NAME ✨

okay so welcome to BUILD(1) bestie, this is the manual for build β€” version 1.5.1 πŸ’…

It's a simple, correct Python packaging frontend (link for the curious: https://packaging.python.org/). Like, that's its whole personality.

What does it actually do? build reads your project's pyproject.toml configuration file (https://packaging.python.org/en/latest/specifications/pyproject-toml/) and invokes build backends to create distribution packages (https://packaging.python.org/en/latest/glossary/#term-Distribution-Package) β€” the files you upload to PyPI (https://pypi.org/) or install with pip. And that's IT. It focuses solely on building packages. It does not manage dependencies. It does not manage virtual environments. It minds its own business, respectfully ✨


🌈 MISSION STATEMENT 🌈

So many Python tools try to do everything at once, and it's giving overachiever. Take pip (https://github.com/pypa/pip) β€” it both installs packages and can build them. Convenient? Sure. But this tight coupling isn't always the vibe.

Some users need standalone build tools for custom environments (outside PyPI β€” https://pypi.org/), or they manage packages themselves (the way Linux distributions do, iconic behaviour honestly).

This project fills that gap: it's a standalone build tool that follows modern Python packaging standards β€” for how build tools communicate with backends, and how pyproject.toml defines build requirements.

We keep dependencies minimal so build stays easy to install and use in restricted environments. Lowkey a hero for that 🫢


πŸ”₯ DIFFERENCES FROM OTHER TOOLS πŸ”₯

Plot twist: thanks to standardization, all compliant build frontends produce the same outputs β€” source distributions (https://packaging.python.org/en/latest/specifications/source-distribution-format/) and wheels (https://packaging.python.org/en/latest/specifications/binary-distribution-format/) β€” from the same project. The differences are mainly in scope, dependencies, and extra features. So like, everyone's wearing the same outfit but accessorising differently πŸ’…

⭐ uv build

uv build (https://docs.astral.sh/uv/) is essentially equivalent to python -m build --installer=uv. Both follow packaging standards, no cap.

But build offers features like --config-json for passing complex nested configuration to backends, and the pip installer works on systems that don't have pre-compiled uv wheels. iykyk.

⭐ setup.py sdist bdist_wheel

build is the modern equivalent of setup.py sdist bdist_wheel, supporting any backend β€” not just setuptools (https://setuptools.pypa.io/). Glow up fr fr.

⭐ hatch build

hatch build (https://hatch.pypa.io/) is the build command from the Hatch project management tool. It's a convenient wrapper around the build process as part of the larger Hatch ecosystem for managing Python projects. Meanwhile build is a standalone tool focused solely on building. Main character energy, but for one specific thing ✨

⭐ flit build

flit build (https://flit.pypa.io/) is the build command from the Flit project. One important difference, and listen up: flit-core produces slightly different source distributions when built by flit itself compared to other frontends. Using build (or any standards-compliant frontend) ensures consistent outputs regardless of the backend. Consistency ate πŸ’…

⭐ cibuildwheel

cibuildwheel (https://cibuildwheel.pypa.io/) is a different kind of tool entirely. While build creates a single wheel for the current platform, cibuildwheel orchestrates building wheels across many platforms and Python versions in CI. It actually calls a build frontend (like build or pip) internally for each platform.

So: use build to create a pure-Python wheel or a single native wheel; use cibuildwheel when you need to produce native wheels for many platforms. Different jobs, bestie πŸŽ€


🌸 WHERE TO START 🌸

First time using build? Start with the Getting Started <> to create your first package.

Need to solve a specific problem? Check the Basic Usage <> for common workflows, or browse the how-to guides below for your specific scenario.

Looking for technical details? The Command-Line Interface <> documents all command-line options, and the API Documentation <> covers the Python API.

Want to understand how it works? Read How it Works <> to learn about the build process and isolation.

πŸ¦‹ Getting Started

This tutorial will guide you through installing build and creating your first Python package ready for distribution. Let's go bestie, we're doing this together 🫢

Note: New to Python packaging? Start with the Python Packaging User Guide tutorial (https://packaging.python.org/en/latest/tutorials/packaging-projects/) to learn how to structure a Python project. This tutorial assumes you already have a project with a pyproject.toml file.

πŸ’œ Prerequisites

You need Python 3.10 or later installed on your system. Check your Python version:

$ python --version
Python 3.11.0

πŸ’œ Installation

The recommended way to install build is using pip:

$ pip install build

Or, if you prefer to install it in an isolated environment using pipx:

$ pipx install build

For corporate environments or systems with restricted internet access, see Corporate Environments <>.

πŸ’œ Creating a simple package

Let's create a minimal Python package to demonstrate how build works. Tiny project, big lessons ✨

  1. Create a project directory:
$ mkdir mypackage
$ cd mypackage
  1. Create a pyproject.toml file:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mypackage"
version = "0.1.0"
description = "A simple example package"
readme = "README.md"
requires-python = ">=3.8"
  1. Create a README.md file:
# My Package

This is a simple example package.
  1. Create your Python package:
$ mkdir src/mypackage
$ touch src/mypackage/__init__.py
  1. Add some code to src/mypackage/__init__.py:
def hello():
    return "Hello from mypackage!"

πŸ’œ Building your package

Now you're ready to build your package:

$ python -m build

You should see output like:

* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling
* Getting build dependencies for wheel...
* Building wheel...
Successfully built mypackage-0.1.0.tar.gz and mypackage-0.1.0-py3-none-any.whl

The built packages are now in the dist/ directory:

$ ls dist/
mypackage-0.1.0-py3-none-any.whl
mypackage-0.1.0.tar.gz

πŸ’œ Understanding what happened

build created two distribution files (https://packaging.python.org/en/latest/glossary/#term-Distribution-Package) β€” packages ready for distribution. Two for the price of one, we love to see it πŸ’–

  1. Source distribution: mypackage-0.1.0.tar.gz

This tarball (https://en.wikipedia.org/wiki/Tar_(computing)) (a compressed archive) contains your source code. Anyone can download it and build your package on their system. Learn more in the source distribution specification (https://packaging.python.org/en/latest/specifications/source-distribution-format/).

  1. Wheel: mypackage-0.1.0-py3-none-any.whl

This wheel (https://packaging.python.org/en/latest/specifications/binary-distribution-format/) is a pre-built package that pip (https://pip.pypa.io/) can install directly without needing to build anything. This makes installation much faster. Fast = iconic.

build used an isolated environment to ensure your package builds consistently regardless of what you have installed on your computer. It:

  1. Created a temporary virtual environment (https://docs.python.org/3/tutorial/venv.html)
  2. Installed only your build dependencies (hatchling β€” https://hatch.pypa.io/latest/)
  3. Invoked the build backend to create the distribution files
  4. Cleaned up the temporary environment when done

So tidy. Left no crumbs 🧚

πŸ’œ Seeing which backend versions were used

build deletes the isolated environment after each build, so it does not leave the installed versions behind. Instead it prints them as it goes:

$ python -m build --wheel
...
* Getting build dependencies for wheel...
* Installed build dependency versions:
  - hatchling==1.30.1
* Building wheel...

build prints each installed build dependency as name==version. Keep this handy when reporting a problem to a backend's issue tracker β€” which usually asks for the exact version you built with. See Troubleshooting <>.

πŸ’œ Building a wheel from your sdist

You can also point build at the source distribution you just produced. build checks the archive and runs a wheel build against its contents, which catches missing files in the sdist:

$ python -m build dist/mypackage-0.1.0.tar.gz
Successfully built mypackage-0.1.0-py3-none-any.whl

The wheel lands next to the sdist. Pass --outdir to write somewhere else.

If your package compiles C/C++ and you rebuild often, --sdist-extract-dir keeps the extracted sources at a fixed path so ccache/sccache can reuse earlier compilations; see Basic Usage <>. Big brain move honestly πŸ”₯

πŸ’œ Building without isolation

If you install the build dependencies yourself and build with --no-isolation, build first checks that every declared dependency is present in the interpreter you are running. When something is missing it stops with an Unmet dependencies error that names the interpreter it checked and, for each requirement, the version it wanted against the version it found β€” so you can tell "not installed" apart from "installed, but the wrong version". See Troubleshooting <> for how to read it.

πŸ’œ Recording what build produced

If a script or CI job needs the exact filenames build produced, add --report PATH to write them to a JSON file instead of parsing the output:

$ python -m build --report build-report.json

πŸ’œ Next steps

  • Learn about Basic Usage <> for common build workflows
  • Understand How it Works <> for details on the build process
  • See Backend Configuration <> to customize your build
  • When a build fails, follow Debug a failed build <#debug-a-failed-build> to capture backend logs and keep the working directories

πŸŽ€ Installation

πŸ’œ Installing build

The recommended way to install build is using uv (https://docs.astral.sh/uv/), but you can also install it via pip (https://pip.pypa.io/) or pipx (https://pipx.pypa.io/):

[uv]

$ uv tool install build

[pipx]

$ pipx install build

[pip]

$ pip install build

πŸ’œ Optional Dependencies

build supports optional extras for different use cases:

$ pip install 'build[virtualenv]'
  • build[uv] β€” bundles uv (https://docs.astral.sh/uv/) so you can use it with --installer=uv even when you've got literally no other install of uv lying around πŸ’…
$ pip install 'build[uv]'

🌈 Verifying Attestations 🌈

Attestations (https://github.com/pypa/build/attestations) exist for versions after 1.2.1, and you can verify them with the gh CLI tool, bestie:

$ python -m pip --no-cache-dir download --no-deps build
$ gh attestation verify build*.whl --repo pypa/build

PEP 740 (https://peps.python.org/pep-0740/) has been accepted, and support for PEP 740 attestations in build is being tracked in #987 (https://github.com/pypa/build/issues/987). We're watching that issue like a season finale πŸ‘€


πŸ₯‘ Installing via conda πŸ₯‘

Over on conda-forge this package goes by the name python-build (https://github.com/conda-forge/python-build-feedstock), so yes you can grab it through conda (https://github.com/conda/conda) or any equivalent:

$ conda install conda-forge::python-build

πŸͺ„ Bootstrapping πŸͺ„

This package can build itself using only the tomli (https://github.com/hukkin/tomli) and pyproject-hooks (https://github.com/pypa/pyproject-hooks) dependencies β€” and ngl, tomli can be omitted entirely on Python 3.11+. In that case you should use the --skip-dependency-check flag ✨


πŸ¦‹ Compatibility πŸ¦‹

build is verified to be compatible with these Python versions, no cap:

  • 3.10
  • 3.11
  • 3.12
  • 3.13
  • 3.14
  • PyPy 3.10
  • PyPy 3.11

πŸ’– BASIC USAGE πŸ’–

This guide covers the most common build commands and workflows. Buckle up, it's giving tutorial era πŸŽ€

🧁 Building both source and wheel distributions (default) 🧁

By default, build creates both a source distribution (https://packaging.python.org/en/latest/specifications/source-distribution-format/) β€” often called an "sdist" β€” and a wheel (https://packaging.python.org/en/latest/specifications/binary-distribution-format/):

$ python -m build

The process, step by step:

  1. Builds the source distribution from your source code
  2. Extracts the source distribution to a temporary directory
  3. Builds the wheel from the extracted source distribution

This two-step process ensures your source distribution contains all necessary files to build your package. If files are missing, the wheel build will fail, alerting you to the problem. It's giving built-in bestie who tells you the truth πŸ™ˆ

This is different from python -m build --sdist --wheel, which builds both directly from source. The default behavior is preferable because it verifies your sdist is complete.

πŸ›ž Building only a wheel πŸ›ž

$ python -m build --wheel

Or the short form:

$ python -m build -w

This builds the wheel (https://packaging.python.org/en/latest/specifications/binary-distribution-format/) β€” the installable package β€” directly from your source directory, skipping the source distribution step. It's faster, but it doesn't verify your source distribution is complete. trade-offs, babe

πŸ“¦ Building only a source distribution πŸ“¦

$ python -m build --sdist

Or the short form:

$ python -m build -s

πŸ—‚οΈ Specifying the source directory πŸ—‚οΈ

Build defaults to the current directory. To build from a different location:

$ python -m build path/to/project

Or explicitly:

$ python -m build --srcdir path/to/project

πŸ«™ Building from a source distribution πŸ«™

The positional argument also accepts a .tar.gz source distribution (https://packaging.python.org/en/latest/specifications/source-distribution-format/). Build checks the filename and the presence of PKG-INFO, extracts the archive into a temporary directory, and runs the wheel build against the extracted source. The wheel lands next to the input archive. Honestly? iconic behavior πŸ’…

$ python -m build dist/mypackage-1.0.0.tar.gz

No flag means the same as --wheel. --metadata works the same way. --sdist errors out, since the archive already is an sdist 🫠

πŸ“ Specifying the output directory πŸ“

By default, distributions are placed in dist/ within the source directory. To use a different location:

$ python -m build --outdir /path/to/output

Or the short form:

$ python -m build -o /path/to/output

⚑ Speeding up rebuilds of compiled packages ⚑

When the default build extracts the intermediate sdist, it uses a random temporary directory. Compiler caches such as ccache (https://ccache.dev/) and sccache (https://github.com/mozilla/sccache) fold the absolute source path into their cache key, so a fresh path on every run means every C/C++ file is recompiled from scratch. Not the vibe. 😭

Pin the extraction path with --sdist-extract-dir so the cache can recognise unchanged files across rebuilds:

$ python -m build --sdist-extract-dir build/sdist

Build creates the directory if missing, clears and repopulates it each run so the contents stay fresh, and keeps it after the build. The path stays stable while the project version is unchanged, as it is during iteration on the extension sources. The same flag applies when building a wheel from a .tar.gz.

Build still runs in a fresh isolated environment whose path is not pinned, so include directories from that environment (Python headers, dependency headers) keep changing. Caching those compile units needs toolchain-side path remapping on top of this flag (e.g. -fdebug-prefix-map, or sccache's base_dir). Fr fr, that part is on your toolchain bestie 🧚

πŸ”Š Controlling verbosity πŸ”Š

Increase verbosity to see more details:

$ python -m build -v

Or even more verbose:

$ python -m build -vv

Decrease verbosity for quieter output:

$ python -m build -q

πŸš€ Using a faster installer πŸš€

By default, build uses pip (https://pip.pypa.io/) to install build dependencies. For faster builds, use uv (https://docs.astral.sh/uv/) β€” a faster alternative to pip:

$ python -m build --installer=uv

This requires uv to be installed:

$ pip install uv

🏝️ Building without isolation 🏝️

By default, build creates an isolated environment (https://packaging.python.org/en/latest/glossary/#term-Isolated-Build) β€” a clean temporary virtual environment (https://docs.python.org/3/tutorial/venv.html) β€” to ensure reproducible builds. To skip this and use your current environment:

$ python -m build --no-isolation

Or the short form:

$ python -m build -n
Warning: When using --no-isolation, you must manually install all build dependencies. This is mainly useful for: - Offline or air-gapped environments (no internet access) - Debugging build issues - Linux distribution packaging where dependencies are provided externally

πŸ™ˆ Skipping dependency checks πŸ™ˆ

To skip checking if build dependencies are installed (requires --no-isolation):

$ python -m build --no-isolation --skip-dependency-check

Or:

$ python -m build -nx

πŸ’ Common workflows πŸ’

🌱 Development build 🌱

Quick build during development:

$ python -m build --wheel --installer=uv

πŸ€– Fast CI build πŸ€–

In CI where dependencies are pre-installed:

$ pip install build build-backend-here
$ python -m build --no-isolation

πŸ‘‘ Release build πŸ‘‘

For uploading to PyPI (https://pypi.org/), build both sdist and wheel. Pick your uploader bestie:

[uv]

$ python -m build
$ uv publish

uv publish (https://docs.astral.sh/uv/guides/publish/) is a modern option that handles uploading to PyPI with built-in support for trusted publishing.

[twine]

$ python -m build
$ twine check dist/*
$ twine upload dist/*

See the twine documentation (https://twine.readthedocs.io/) for upload options.

Tip: Use the hynek/build-and-inspect-python-package GitHub Action which handles this workflow including verification. See CI/CD Integration <>.

πŸ” Testing the sdist πŸ”

Build from the sdist to ensure it's complete:

$ python -m build --sdist
$ python -m build dist/mypackage-1.0.0.tar.gz

Or test installation:

$ python -m build
$ python -m pip install dist/mypackage-1.0.0.tar.gz
$ python -c "import mypackage; print(mypackage.__version__)"

🧼 Cleaning before build 🧼

There's no built-in clean command. To ensure a fresh build, manually remove the dist directory:

$ rm -rf dist/
$ python -m build

Or to avoid stale artifacts, use a unique output directory:

$ python -m build --outdir dist/v1.0.0

πŸ“œ Getting metadata without building πŸ“œ

To extract package metadata without building the full package:

$ python -m build --metadata

This outputs the wheel metadata in JSON format to stdout. data? served. ✨

🏷️ Checking the build version 🏷️

To see which version of build you're using:

$ python -m build --version

πŸ› οΈ Installing build dependencies only πŸ› οΈ

For specialized workflows like static analysis or linting, you may want to install just the build dependencies without actually building:

from build import ProjectBuilder

builder = ProjectBuilder(".")

# Get all build dependencies
requires = builder.build_system_requires
for dist in ["sdist", "wheel"]:
    requires.extend(builder.get_requires_for_build(dist))

# Install them
import subprocess

subprocess.run(["pip", "install", *requires])

This is useful when you need the same environment that build would create, but want to run other tools (like mypy, ruff, or custom linters) instead of building the package.

See API Documentation <> for more programmatic usage examples.

πŸ‘€ See also πŸ‘€

  • Backend Configuration <> for passing options to your build backend
  • Corporate Environments <> for using build with private indexes
  • Troubleshooting <> for common problems
  • Command-Line Interface <> for all command-line options

🌈 build vs Other Tools 🌈

The Python packaging ecosystem has several tools that work together. This guide helps you understand when to use build versus other tools. It's giving the group project breakdown πŸ’…

πŸ’β€β™€οΈ When to use build πŸ’β€β™€οΈ

Use build when you want to:

Build is a build frontend β€” it knows how to invoke build backends (setuptools (https://setuptools.pypa.io/), hatchling (https://hatch.pypa.io/latest/), flit (https://flit.pypa.io/), etc.) but doesn't handle testing, multi-version builds, or task automation. She's focused, okay? πŸ™ˆ

$ python -m build

This builds your package for the currently active Python interpreter.

🎑 When to use cibuildwheel 🎑

Use cibuildwheel (https://cibuildwheel.readthedocs.io/) when you need to build wheels (https://packaging.python.org/en/latest/specifications/binary-distribution-format/) for multiple Python versions and platforms:

  • Building wheels for distribution on PyPI (https://pypi.org/)
  • Supporting Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14
  • Supporting multiple operating systems β€” Linux, macOS, Windows, the whole squad πŸ’–
  • Building manylinux / musllinux wheels β€” those are the standardized Linux wheel formats, and yes bestie they have their own PEPs: https://peps.python.org/pep-0513/ and https://peps.python.org/pep-0656/ πŸŽ€
  • Building wheels with compiled extensions β€” C, C++, Rust, etc. all welcome ✨

So like, cibuildwheel literally uses build under the hood but it handles all the complexity for you, no cap:

Example .github/workflows/build.yml:

name: Build

on: [push, pull_request]

jobs:
  build_wheels:
    name: Build wheels on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]

    steps:
      - uses: actions/checkout@v4
      - uses: pypa/cibuildwheel@v2.17
      - uses: actions/upload-artifact@v4
        with:
          name: wheels
          path: ./wheelhouse/*.whl

Full deets are in the cibuildwheel documentation, https://cibuildwheel.readthedocs.io/ πŸ’…


When to use tox πŸ’œ

Use tox β€” https://tox.wiki/ β€” when you need to:

  • Test your package across multiple Python versions (like 3.10, 3.11, 3.12, 3.13, 3.14) 🌈
  • Run tests in isolated environments β€” https://packaging.python.org/en/latest/glossary/#term-Virtual-Environment 🧚
  • Automate testing workflows
  • Run linters (aka code checkers), formatters, type checkers
  • Build documentation
  • Orchestrate multiple development tasks

Example tox.toml:

[env_run_base]
description = "run test suite with {basepython}"
deps = [
    "pytest",
    "pytest-cov",
]
commands = [
    ["pytest", "tests"],
]

[env.build]
description = "build the package"
deps = ["build"]
commands = [["python", "-m", "build"]]

[env.lint]
description = "run linters and type checkers"
deps = [
    "ruff",
    "mypy",
]
commands = [
    ["ruff", "check", "."],
    ["mypy", "src"],
]

Tox can call build to create distributions, but its main purpose is test automation, just so you know 🫢

See the tox documentation at https://tox.wiki/ for complete details.

When to use nox 🌸

Use nox β€” https://nox.thea.codes/ β€” when you want tox-like functionality but you'd rather write Python than INI, iykyk:

Example noxfile.py:

import nox

@nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"])
def tests(session):
    session.install("pytest", "pytest-cov")
    session.run("pytest", "tests")

@nox.session
def build(session):
    session.install("build")
    session.run("python", "-m", "build")

See the nox documentation at https://nox.thea.codes/ for complete details ✨

When to use uv ⭐

Use uv β€” https://docs.astral.sh/uv/ β€” when you want:

  • Fast package installation (faster than pip, fr fr)
  • Workspace management (monorepos with multiple packages)
  • Combined dependency resolution and installation
  • Modern Python packaging workflows

uv can also build packages:

$ uv build

This is equivalent to python -m build --installer=uv πŸ’…

See the uv documentation at https://docs.astral.sh/uv/ for complete details.

When to use pip πŸŽ€

Use pip when you need to:

  • Install packages (not build them, bestie, that's a different job)
  • Install from PyPI or other indexes
  • Install in editable mode for development
  • Manage dependencies in your environment

pip can build packages as a side effect of installation, but for explicit building, use build. Periodt.

# Installing (use pip)
$ pip install mypackage

# Installing in editable mode (use pip)
$ pip install -e .

# Building distributions (use build)
$ python -m build

How they work together 🀝

A typical complete workflow might use all these tools:

Development workflow

# Install your package in editable mode
$ pip install -e .[dev]

# Run tests across Python versions
$ tox

# Build distributions
$ python -m build

# Upload to PyPI
$ twine upload dist/*

CI/CD workflow

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
      - run: pip install tox
      - run: tox -e py

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hynek/build-and-inspect-python-package@v2

  build_wheels:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: pypa/cibuildwheel@v2.17

This workflow:

  1. Uses tox to test across Python versions
  2. Uses build (via hynek's action) to create sdist
  3. Uses cibuildwheel to create wheels for all platforms

Decision tree 🌳

[graph].SS Quick reference
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Tool         β”‚ Purpose              β”‚ Use for              β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚ build        β”‚ Build sdist/wheel    β”‚ Creating             β”‚
    β”‚              β”‚ for one Python       β”‚ distributions        β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚ cibuildwheel β”‚ Build wheels for all β”‚ Distributing on PyPI β”‚
    β”‚              β”‚ platforms            β”‚                      β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚ tox          β”‚ Test across Python   β”‚ CI/testing           β”‚
    β”‚              β”‚ versions             β”‚ automation           β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚ nox          β”‚ Like tox, but        β”‚ CI/testing           β”‚
    β”‚              β”‚ Python-based         β”‚ automation           β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚ uv           β”‚ Fast package manager β”‚ Installing +         β”‚
    β”‚              β”‚                      β”‚ optional building    β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚ pip          β”‚ Package installer    β”‚ Installing packages  β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚ twine        β”‚ Upload to PyPI       β”‚ Publishing           β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

See also πŸ‘€

  • Basic Usage <> for build command examples
  • CI/CD Integration <> for CI/CD integration
  • Build Backends <> to understand build vs backend

CI/CD Integration πŸš€

This guide shows how to integrate build into your continuous integration and deployment workflows. Slay.

GitHub Actions πŸ™

Using hynek's build-and-inspect-python-package

The recommended way to build and verify packages in GitHub Actions β€” https://docs.github.com/en/actions β€” is using the hynek/build-and-inspect-python-package action, https://github.com/hynek/build-and-inspect-python-package. This action builds your package, inspects it for common issues, and provides the built artifacts. Iconic behaviour honestly.

name: Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hynek/build-and-inspect-python-package@v2

This action automatically:

  • Builds both sdist and wheel ✨
  • Verifies the sdist contains all necessary files
  • Checks for common packaging issues
  • Uploads artifacts for download
  • Works with any PEP 517 build backend

For more details, see the action documentation at https://github.com/hynek/build-and-inspect-python-package πŸ’–

Manual build with isolation 🧼

If you need more control, use build directly with isolated environments. pipx is pre-installed on all GitHub Actions runners:

name: Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Build package
        run: pipx run build

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

Fast builds with pre-installed dependencies ⚑

For faster CI builds, pre-install the build dependencies and use --no-isolation:

name: Fast Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: |
          pip install build hatchling  # Replace hatchling with your backend

      - name: Build package
        run: python -m build --no-isolation

Using uv for faster builds πŸ¦‹

Use uv as the installer for faster dependency installation:

name: Build with uv

…and the rest of that workflow file wraps up like this, bestie πŸ’–

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install uv and build
        run: pip install uv build

      - name: Build package
        run: python -m build --installer=uv

✨ Matrix builds across Python versions ✨

Wanna test that your build survives on multiple Python versions? Say less:

name: Build Matrix

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install build
        run: pip install build

      - name: Build package
        run: python -m build

πŸ¦‹ Building wheels for multiple platforms πŸ¦‹

Got C extensions or platform-specific code? Then it's cibuildwheel o'clock:

name: Build Wheels

on: [push, pull_request]

jobs:
  build_wheels:
    name: Build wheels on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-13, macos-14]

    steps:
      - uses: actions/checkout@v4

      - uses: pypa/cibuildwheel@v2.17

      - uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.os }}
          path: ./wheelhouse/*.whl

  build_sdist:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hynek/build-and-inspect-python-package@v2

Peep build vs Other Tools to figure out when you want build and when you want cibuildwheel πŸ‘€

πŸ’… Publishing to PyPI πŸ’…

You can build and publish in one release workflow using the PyPA publish action (https://github.com/pypa/gh-action-pypi-publish):

name: Publish

on:
  release:
    types: [published]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hynek/build-and-inspect-python-package@v2

  publish:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: Packages
          path: dist

      - uses: pypa/gh-action-pypi-publish@release/v1

🌸 GitLab CI 🌸

Basic build

build:
  image: python:3.12
  script:
    - pip install build
    - python -m build
  artifacts:
    paths:
      - dist/

With caching

Cache those pip packages so your builds stop taking a million years:

build:
  image: python:3.12
  cache:
    paths:
      - .cache/pip
  variables:
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  script:
    - pip install build
    - python -m build
  artifacts:
    paths:
      - dist/

Fast builds with pre-installed dependencies

build:
  image: python:3.12
  script:
    - pip install build hatchling
    - python -m build --no-isolation
  artifacts:
    paths:
      - dist/

Matrix builds

.build_template:
  script:
    - pip install build
    - python -m build
  artifacts:
    paths:
      - dist/

build:py310:
  extends: .build_template
  image: python:3.10

build:py314:
  extends: .build_template
  image: python:3.14

πŸŽ€ CircleCI πŸŽ€

Basic build

version: 2.1

jobs:
  build:
    docker:
      - image: cimg/python:3.12
    steps:
      - checkout
      - run:
          name: Install build
          command: pip install build
      - run:
          name: Build package
          command: python -m build
      - store_artifacts:
          path: dist/

workflows:
  build_workflow:
    jobs:
      - build

⭐ Travis CI ⭐

Basic build

language: python
python:
  - "3.10"
  - "3.11"
  - "3.12"
  - "3.13"
  - "3.14"

install:
  - pip install build

script:
  - python -m build

🧚 Azure Pipelines 🧚

Basic build

trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

strategy:
  matrix:
    Python310:
      python.version: '3.10'
    Python314:
      python.version: '3.14'

steps:
- task: UsePythonVersion@0
  inputs:
    versionSpec: '$(python.version)'

- script: |
    pip install build
    python -m build
  displayName: 'Build package'

- task: PublishBuildArtifacts@1
  inputs:
    pathToPublish: 'dist'
    artifactName: 'dist'

πŸ‘‘ Jenkins πŸ‘‘

Declarative pipeline

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                sh '''
                    python3 -m pip install build
                    python3 -m build
                '''
            }
        }
        stage('Archive') {
            steps {
                archiveArtifacts artifacts: 'dist/*', fingerprint: true
            }
        }
    }
}

πŸ“ Docker πŸ“

Multi-stage build

Build your packages inside a Docker container, iconic behaviour:

FROM python:3.12-slim AS builder

WORKDIR /app

COPY pyproject.toml README.md ./
COPY src/ src/

RUN pip install build && python -m build

# Runtime image
FROM python:3.12-slim

COPY --from=builder /app/dist/*.whl /tmp/
RUN pip install /tmp/*.whl && rm /tmp/*.whl

CMD ["python"]

🌈 tox Integration 🌈

Running build inside tox environments, no cap:

[env_run_base]
description = "run test suite"
deps = ["pytest"]
commands = [["pytest", "tests"]]

[env.build]
description = "build the package"
deps = ["build"]
commands = [["python", "-m", "build"]]

[env."build-check"]
description = "build and check the package"
deps = [
    "build",
    "twine",
]
commands = [
    ["python", "-m", "build"],
    ["twine", "check", "dist/*"],
]

And to actually run that build environment:

$ tox -e build

πŸ’œ Discovering Built Artifacts πŸ’œ

Okay so this one is important, bestie. Artifact filenames are dynamic, which means pipelines that need to act on the exact files the build produced (uploading, signing, computing digests) should not guess them or glob them. Instead, pass --report PATH to capture a machine-readable list, then read it back with a JSON tool like jq. And pick a PATH that lives outside the output directory, so tools that slurp up dist/* wholesale don't trip over it:

- name: Build and upload
  run: |
    python -m build --report build-report.json
    twine upload $(jq -r '.artifacts[].path' build-report.json)

The report also includes each artifact's size and SHA-256 hash, which is super useful for emitting provenance or checksums:

$ jq -r '.artifacts[] | "\(.hashes.sha256)  \(.name)"' build-report.json

See Command-Line Interface for the full report schema 🫢

✨ Verifying Builds ✨

Always verify your built packages before you publish. Always. Not negotiable:

- name: Check package
  run: |
    pip install twine
    twine check dist/*

Or use hynek's action, which has the verification baked in automatically 😌

πŸ¦‹ Conditional Builds πŸ¦‹

Only build on specific events? Gotchu:

name: Build

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hynek/build-and-inspect-python-package@v2

  publish:
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
      - uses: pypa/gh-action-pypi-publish@release/v1

So this builds on every push and PR, but only publishes when you push a version tag. Main character energy πŸŽ€

😭 Troubleshooting CI Builds 😭

Enable verbose output

Throw in the -vv flag when you're debugging:

- name: Build package
  run: python -m build -vv

Preserve build artifacts

Upload the dist directory even when things go wrong:

- name: Upload artifacts
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/

Cache build environments

If you're building over and over, cache pip's download cache:

- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}

πŸ’– See also πŸ’–


🌸 Corporate Environments 🌸

This guide covers the common scenarios you'll hit when using build behind corporate firewalls, with private package indexes (aka custom package repositories), or in restricted network environments.

πŸ’… Private package indexes πŸ’…

Using a private PyPI mirror

Set the PIP_INDEX_URL environment variable (https://pip.pypa.io/en/stable/topics/configuration/#environment-variables) to point at your private package index (https://packaging.python.org/en/latest/glossary/#term-Package-Index) β€” which is, y'know, an alternative to PyPI (https://pypi.org/):

$ export PIP_INDEX_URL=https://pypi.company.com/simple
$ python -m build

Or if you just want it for a single command:

$ PIP_INDEX_URL=https://pypi.company.com/simple python -m build

Additional indexes

Wanna use both PyPI and a private index? Periodt:

$ export PIP_INDEX_URL=https://pypi.org/simple
$ export PIP_EXTRA_INDEX_URL=https://pypi.company.com/simple
$ python -m build
🚨 Warning: PIP_EXTRA_INDEX_URL is lowkey a security situation, bestie. A malicious package chilling on any index can shadow packages from other indexes. Use PIP_INDEX_URL exclusively whenever you can. That's the move. πŸ”’

πŸ” Authentication

πŸͺͺ Embedded credentials

You can yeet the credentials straight into the index URL:

$ PIP_INDEX_URL=https://username:password@pypi.company.com/simple python -m build
⚠️ Warning: embedded credentials are giving "leaked password in shell history and CI logs". Use environment variable substitution when you can:
$ PIP_INDEX_URL=https://${PYPI_USER}:${PYPI_TOKEN}@pypi.company.com/simple python -m build

πŸ”‘ Using keyring

build passes --no-input to pip, which blocks those hidden credential prompts that make the process look like it's frozen forever 😭. And when the keyring CLI is available on PATH, build automatically sets PIP_KEYRING_PROVIDER=subprocess (or UV_KEYRING_PROVIDER=subprocess when you're using the uv installer), so the installer delegates credential lookups to the system keyring β€” no keyring needed inside the isolated build environment. Iconic. ✨

Install keyring system-wide so it's available on PATH:

$ pipx install keyring

Or install build with the keyring extra:

$ pip install build[keyring]

Then store your credentials:

$ keyring set https://pypi.company.com username

For specialized backends (e.g., Google Artifact Registry, Azure Artifacts), install the corresponding keyring plugin alongside keyring:

$ pipx install keyring
$ pipx inject keyring keyrings.google-artifactregistry-auth

You can also set the keyring provider explicitly via environment variable β€” super useful in CI, or when keyring lives in a non-standard location:

$ export PIP_KEYRING_PROVIDER=subprocess  # for pip installer
$ export UV_KEYRING_PROVIDER=subprocess   # for uv installer
$ python -m build

To disable keyring entirely:

$ export PIP_KEYRING_PROVIDER=disabled
$ export UV_KEYRING_PROVIDER=disabled
$ python -m build

πŸ“œ Using .netrc

Make a ~/.netrc file (~/_netrc on Windows) with your credentials in it:

machine pypi.company.com
login username
password your-password

Make sure that file has restricted permissions (we are NOT sharing our secrets with the whole group chat):

$ chmod 600 ~/.netrc

πŸ”’ SSL/TLS certificates

🏒 Custom CA certificates

If your company runs a custom CA certificate:

$ export REQUESTS_CA_BUNDLE=/path/to/company-ca-bundle.crt
$ python -m build

Or use pip's certificate option:

$ export PIP_CERT=/path/to/company-ca-bundle.crt
$ python -m build

πŸ™ˆ Self-signed certificates

For development environments with self-signed certificates β€” and heads up, this is not recommended for production, bestie, this is a sandbox-only vibe:

$ export PIP_TRUSTED_HOST=pypi.company.com
$ python -m build

🧚 Using virtualenv for better SSL support

If you keep catching SSL/TLS errors with Python's built-in venv, install virtualenv:

$ pip install build[virtualenv]

That hooks you up with a more recent version of pip that has better SSL support and truststore capabilities. Glow up πŸ’…

πŸ“ Note: Python 3.11.8+ and 3.12.2+ already have improved SSL support and may not need this workaround. Iykyk.

🌐 Proxy servers

πŸ”€ HTTP/HTTPS proxies

Configure the proxy environment variables:

$ export HTTP_PROXY=http://proxy.company.com:8080
$ export HTTPS_PROXY=http://proxy.company.com:8080
$ python -m build

With authentication:

$ export HTTP_PROXY=http://user:pass@proxy.company.com:8080
$ export HTTPS_PROXY=http://user:pass@proxy.company.com:8080

🚫 No proxy for certain hosts

$ export NO_PROXY=localhost,127.0.0.1,.company.com
$ python -m build

βš™οΈ pip configuration files

🌍 Global configuration

Instead of environment variables, you can create a pip configuration file.

On Linux/macOS (~/.config/pip/pip.conf):

[global]
index-url = https://pypi.company.com/simple
trusted-host = pypi.company.com
cert = /path/to/company-ca-bundle.crt

On Windows (%APPDATA%\\pip\\pip.ini):

[global]
index-url = https://pypi.company.com/simple
trusted-host = pypi.company.com
cert = C:\\path\\to\\company-ca-bundle.crt

πŸ“ Per-project configuration

Create a pip.conf file in your project directory. This one's a whole serve for team settings β€” everybody gets the same config, no drama. 🫢

✈️ Air-gapped environments

For completely offline builds, you have to manually install dependencies first:

$ pip install setuptools wheel your-build-backend
$ python -m build --no-isolation

The --no-isolation flag tells build to use your current environment instead of creating an isolated one.

⚠️ Warning: when you're using --no-isolation, make sure all build dependencies are installed and compatible. No half-finished setups allowed. πŸ’€

πŸ€– CI/CD environments

πŸ™ GitHub Actions

- name: Build package
  run: python -m build
  env:
    PIP_INDEX_URL: ${{ secrets.PYPI_INDEX_URL }}
    PIP_CERT: /path/to/cert.crt

🦊 GitLab CI

build:
  script:
    - export PIP_INDEX_URL=$PYPI_INDEX_URL
    - python -m build

🚨 Catching backend warnings

To catch deprecation warnings from your build backend (absolute must in CI):

$ PYTHONWARNINGS=error::DeprecationWarning python -m build

Or for setuptools specifically:

$ PYTHONWARNINGS=error:::setuptools.config.setupcfg python -m build

See Troubleshooting <> for more details on warnings.

πŸ’€ Common issues

πŸ”‘ Build fails with authentication errors

build passes --no-input to pip to prevent hidden credential prompts. If your private index requires authentication, pip will fail instead of hanging there looking stuck. Configure one of the authentication methods above (embedded credentials, keyring, or .netrc) to provide credentials non-interactively. Periodt.

🚫 401/403 errors

Authentication failed, bestie. Verify your credentials and make sure they actually have access to the required packages.

πŸ”’ SSL verification failed

Your system doesn't trust the certificate. Use REQUESTS_CA_BUNDLE or PIP_CERT to provide the CA certificate, or install build[virtualenv] for better SSL support.

🐍 Conda environment conflicts

If you're in a conda environment and you're getting venv creation issues:

$ conda deactivate
$ python -m build

Or use virtualenv:

$ pip install build[virtualenv]
$ python -m build

πŸ‘€ See also

  • Troubleshooting <> for more problem-solving tips
  • Environment Variables <> for a complete list of environment variables
  • pip documentation <https://pip.pypa.io/> for more pip configuration options

πŸ› οΈ Backend Configuration

The --config-setting (or -C) flag lets you pass options to your build backend. The syntax and the available options depend entirely on which backend you're using (e.g., setuptools <https://setuptools.pypa.io/>, hatchling, flit <https://flit.pypa.io/>). Choose your fighter. πŸ₯Š

⚠️ Important: the --config-setting flag passes options to the build backend that actually builds your package, not to build itself. Different backends accept different options. Check your backend's documentation for available settings.

✍️ Basic syntax

The basic syntax is:

$ python -m build -C KEY=VALUE

Or for options without values:

$ python -m build -C KEY=
πŸ“ Note: build also accepts the bare -C KEY form (without =) and reads it as -C KEY=, but it warns you because pip rejects it. Write -C KEY= to keep the command working in both tools.

Multiple settings can be provided:

$ python -m build -C KEY1=VALUE1 -C KEY2=VALUE2

🧱 Setuptools

Setuptools requires a special --build-option wrapper for most settings.

πŸ”’ Build numbers

To add a build number to your wheel:

$ python -m build --wheel \
    -C--build-option=bdist_wheel \
    -C--build-option=--build-number \
    -C--build-option=123

This creates a wheel named package-1.0.0-123-py3-none-any.whl.

πŸ› οΈ Compiler selection

To specify a compiler on Windows:

$ python -m build \
    -C--build-option=build_ext \
    -C--build-option=--compiler \
    -C--build-option=mingw32
πŸ“ Note: each argument must be a separate -C flag. Setuptools processes these as separate command-line arguments.

πŸ’¬ Getting help

To see available setuptools build options:

$ python -m build -C--build-option=--help

🧊 scikit-build-core

scikit-build-core uses a more intuitive dotted syntax.

πŸ—οΈ CMake options

$ python -m build \
    -Ccmake.define.BUILD_TESTING=OFF \
    -Ccmake.define.CMAKE_BUILD_TYPE=Release

πŸ“Š Logging

$ python -m build -Clogging.level=INFO

πŸ“¦ Install target

$ python -m build -Cinstall.strip=false

Multiple options:

$ python -m build \
    -Clogging.level=DEBUG \
    -Ccmake.define.USE_CUDA=ON \
    -Cinstall.components=runtime

🌌 meson-python

Meson-python passes options directly to meson.

βš™οΈ Setup options

$ python -m build -Csetup-args=-Doption=value

πŸ”§ Compile options

$ python -m build \
    -Csetup-args=-Dbuildtype=release \
    -Csetup-args=-Db_ndebug=true

🐣 hatchling

Hatchling supports various build options.

πŸ” Reproducible builds

$ python -m build -Creproducible=true

πŸ“‚ Shared data

$ python -m build -Cshared-data=/custom/path

πŸͺΆ flit-core

Flit-core has minimal configuration options as it focuses on simplicity.

🎭 poetry-core

Poetry-core currently does not support configuration settings via the command line.

🧩 Using --config-json

For complex nested configuration, use --config-json instead:

$ python -m build --config-json='{"cmake": {"define": {"VAR1": "value1", "VAR2": "value2"}}}'

This is particularly useful in CI/CD scripts where configuration can be generated programmatically.

πŸ“ Note: --config-setting and --config-json cannot be used together.

⚠️ Common Issues

βž– Options starting with hyphens

If your option starts with a hyphen, you must use the = syntax to prevent build from interpreting it as its own flag:

$ python -m build -C=--my-setting

Not -C --my-setting which would fail.

πŸ€” Backend not recognizing options

If your backend doesn't recognize an option:

  1. Check your backend's documentation for the correct syntax
  2. Verify you're using the latest version of the backend
  3. Some backends only support certain options in newer versions

πŸ“š Finding backend documentation

πŸ‘€ See also

  • Basic Usage <> for general build commands
  • Command-Line Interface <> for all command-line options
  • Build Backends <> to understand how backends work

🩹 Troubleshooting

This guide helps you resolve common issues when using build.

✨ Build fails with missing dependencies ✨

Symptom: your build dies with stuff like "ModuleNotFoundError" or "No module named 'X'". πŸ’€

Cause: bestie, your build backend (or its lil dependency friends) just isn't installed in that isolated environment.

Solution 1 β€” let build handle it automatically (this is the default, so just vibe):

$ python -m build

build will read your pyproject.toml and install all the required dependencies for you. slay. πŸ’…

Solution 2 β€” if you're using --no-isolation, you gotta install the deps yourself:

$ pip install setuptools your-build-backend
$ python -m build --no-isolation

πŸ’€ "Unmet dependencies" with --no-isolation πŸ’€

Symptom: a --no-isolation build just stops and hits you with:

ERROR Unmet dependencies (checked against /usr/local/bin/python3.9):
    anndata>=0.7.4
        wanted: >=0.7.4
        found: not installed

Cause: okay so this one's important β€” build checks the declared requirements against the interpreter named in the header (the one running build), not against a virtual environment or your system package manager. So read each entry like this:

  • found: not installed β€” the package is legit invisible to that interpreter's metadata. If you're sitting there like "but I installed it tho" (say, via a Linux distribution or a FreeBSD port), it got installed for a different Python, so build literally cannot see it. Fix: install it for the interpreter in the header, or run that interpreter's python -m build.
  • found: <version> β€” the package is installed, but its version doesn't satisfy wanted. So upgrade or downgrade it to match. no cap.

Solution 1: install or fix the offending dependency for the interpreter shown in the header.

Solution 2: if you manage build dependencies externally (distribution packaging girlies, this is you), just skip the check entirely:

$ python -m build --no-isolation --skip-dependency-check

See Basic Usage <> for more on --skip-dependency-check. 🫢


πŸ” Find the backend version for a bug report πŸ”

Symptom: a backend's issue tracker is asking for the exact version of the backend you used. But pyproject.toml only lists the requirement specifiers (setuptools >= 40.8.0), and on an isolated build the resolved version lives in the temporary environment that build deletes afterwards. rude. 😭

Solution: read it from build's output! After installing the build dependencies, build prints the resolved version of each one:

$ python -m build --wheel
...
* Getting build dependencies for wheel...
* Installed build dependency versions:
  - setuptools==82.0.1
* Building wheel...

Copy that name==version line for the backend straight into your report. ✨ With --no-isolation build installs nothing, so read the version from the active interpreter instead (for example pip show setuptools).


🧊 Build hangs or appears frozen 🧊

Symptom: the build command runs but produces no output for a long, long time. the silence is deafening fr.

Possible causes:

  1. Waiting for authentication: if you're using a private package index, build may be waiting for credentials.

See Corporate Environments <> for authentication setup.

  1. Large download: the build backend dependencies are being downloaded.

Use -v or -vv for verbose output:

$ python -m build -vv
  1. Build backend is actually running: some backends (especially for C extensions) just take time, bestie.

Check your system monitor for CPU/disk activity to confirm work is actually happening.


πŸ”’ SSL certificate verification failed πŸ”’

Symptom: errors like "SSL: CERTIFICATE_VERIFY_FAILED" or "certificate verify failed".

Cause: your system doesn't trust the SSL certificate of the package index. distrust era. πŸ™ˆ

Solution 1 β€” provide the CA certificate (recommended):

$ export PIP_CERT=/path/to/company-ca-bundle.crt
$ python -m build

Solution 2 β€” use build with virtualenv for better SSL support:

$ pip install build[virtualenv]
$ python -m build

Solution 3 β€” for development only, disable verification (not recommended):

$ export PIP_TRUSTED_HOST=pypi.company.com
$ python -m build

See Corporate Environments <> for more details.


🚫 Permission denied errors 🚫

Symptom: "PermissionError" or "Access is denied" when creating directories or files.

Common causes:

  1. Output directory is protected: the dist/ directory is owned by another user or process.

Solution: remove the directory first, or use a different output location:

$ rm -rf dist/
$ python -m build

Or:

$ python -m build --outdir /tmp/my-build
  1. Running inside a read-only directory: the source directory is on a read-only filesystem.

Solution: copy the source to a writable location first.

  1. Antivirus software: some antivirus tools block Python from creating virtual environments. πŸ’€

Solution: add Python and your project directory to your antivirus exclusions.


πŸ›ž Wheel is not compatible with this platform πŸ›ž

Symptom: "is not a supported wheel on this platform" when you try to install the built wheel.

Cause: the wheel was built for a different Python version or platform. it's giving mismatched energy.

Solution: build on the same platform and Python version where you'll install it, or use cibuildwheel for multi-platform builds.

See build vs Other Tools <> for when to use cibuildwheel vs build.


πŸŽ€ Build succeeds but package is missing files πŸŽ€

Symptom: after building, the package installs but is missing source files, data files, or modules. where did they go 😭

Cause: the files aren't included in the source distribution manifest.

Solution: the fix depends on your build backend, so let's go:

For setuptools (setuptools documentation <https://setuptools.pypa.io/>):

Add a MANIFEST.in file, or use package_data in pyproject.toml:

[tool.setuptools.package-data]
mypackage = ["data/*.json", "templates/*.html"]

For hatchling (hatchling documentation <https://hatch.pypa.io/latest/config/build/>):

Use the include and exclude options:

[tool.hatch.build.targets.wheel]
include = [
    "src/**/*.py",
    "src/**/*.json",
]

For flit (flit documentation <https://flit.pypa.io/>):

Flit includes all files tracked by git <https://git-scm.com/> by default. So make sure your files are committed, bestie.

Test your sdist: always verify the sdist contains all the necessary files:

$ python -m build --sdist
$ tar -tzf dist/mypackage-1.0.0.tar.gz

😡 ModuleNotFoundError in the built wheel 😡

Symptom: the wheel installs successfully, but importing the package fails with "ModuleNotFoundError".

Cause: the package structure in your source doesn't match what the build backend expects.

Common issues:

  1. Missing __init__.py: ensure every package directory has an __init__.py file (not needed for namespace packages).
  2. Wrong directory structure: for src-layout, ensure you have src/packagename/ not just packagename/.
  3. Not configuring package discovery: tell your build backend where to find packages.

For setuptools with src-layout:

[tool.setuptools.packages.find]
where = ["src"]

βš”οΈ Version conflict between build dependencies βš”οΈ

Symptom: the build fails with errors about conflicting package versions. drama in the dependency tree, honestly.

Cause: build dependencies have incompatible version requirements.

Solution 1 β€” update your build backend and its dependencies:

$ pip install --upgrade pip setuptools

Then try building again:

$ python -m build

Solution 2 β€” if using --no-isolation, create a fresh virtual environment:

$ python -m venv fresh-env
$ source fresh-env/bin/activate  # On Windows: fresh-env\\Scripts\\activate
$ pip install build
$ python -m build

The isolated build (without --no-isolation) avoids this issue by creating a clean environment each time. clean girl aesthetic 🧚


🏠 Build works locally but fails in CI 🏠

Common causes:

  1. Different Python versions: CI may use a different Python version than your local environment.

Solution: specify the Python version in your CI config, and test locally with that version.

  1. Missing system dependencies: C extensions may need system libraries.

Solution: install the required libraries in CI before building:

# GitHub Actions example
- name: Install system dependencies
  run: |
    sudo apt-get update
    sudo apt-get install -y libffi-dev
  1. File path issues: Windows vs Unix path separators. the eternal struggle.

Solution: use pathlib.Path in pyproject.toml configurations and build scripts.

  1. Git not available: some backends use git for version detection.

Solution: ensure git is installed in CI, or use explicit versioning in pyproject.toml.

See CI/CD Integration <> for CI-specific guidance.


🚨 Catching deprecation warnings 🚨

To catch warnings from your build backend in CI:

$ PYTHONWARNINGS=error::DeprecationWarning python -m build

Or for setuptools specifically:

$ PYTHONWARNINGS=error:::setuptools.config.setupcfg python -m build

This fails the build if deprecated features are used, helping you catch issues early. iconic behavior honestly.


πŸ‘» Build backend not found πŸ‘»

Symptom: "Backend 'X' is not available" or "No module named 'X'".

Cause: the build backend specified in pyproject.toml isn't installed.

Solution: this shouldn't happen with isolated builds (the default). But if you do see it:

  1. Check your pyproject.toml [build-system] section is correct:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
  1. If using --no-isolation, install the backend:
$ pip install hatchling
$ python -m build --no-isolation

πŸ“„ No build-system section in pyproject.toml πŸ“„

Symptom: the build succeeds but you're not sure which backend is being used. mystery backend era.

Cause: no [build-system] section is specified in pyproject.toml.

Solution: as recommended in PEP 517 <https://peps.python.org/pep-0517/>, if no backend is specified, build will fallback to setuptools.build_meta:__legacy__. This is for backward compatibility with older projects. For new projects, explicitly specify your build backend:

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

This makes your build configuration explicit and avoids relying on the fallback behavior. periodt.


🏚️ Build installed in user site-packages 🏚️

Symptom: virtual environment creation fails with errors like "executable /usr/local/bin/python missing".

Cause: when build is installed in the user site-packages (pip install --user build), it can interfere with isolated environment creation.

Solution: install build in a virtual environment instead:

$ python -m venv build-env
$ source build-env/bin/activate  # On Windows: build-env\\Scripts\\activate
$ pip install build
$ python -m build

Or use pipx to install build in isolation:

$ pipx install build
$ pipx run build

🎭 Package name has dashes but wheel has underscores 🎭

Symptom: your package is named my-package but the wheel is my_package-1.0.0-py3-none-any.whl.

This is expected behavior: According to PEP 427 (https://peps.python.org/pep-0427/), wheel filenames normalize package names by replacing dashes with underscores. This is not a bug. πŸ’…

Your users can still install with either name:

$ pip install my-package    # Works
$ pip install my_package    # Also works

The package metadata preserves your original name, only the filename is normalized. πŸŽ€


🌸 Getting more information 🌸

πŸ—£οΈ Verbose output

Use -v or -vv for detailed output:

$ python -m build -v      # Verbose
$ python -m build -vv     # Very verbose

This shows what build is doing and can help identify where failures occur. Receipts, bestie. 🧾

πŸ” Check your configuration

Verify your pyproject.toml is valid:

$ python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb')))"

πŸ§ͺ Test the build backend directly

You can test if your build backend works independently:

$ pip install your-build-backend
$ python -m your_build_backend

Vibe check the backend on its own so you know whether it's your project or your backend acting up. πŸ™ˆ

πŸ”¬ Inspect the build environment

build removes its isolated environment when the build ends. To keep it for inspection, point --env-dir at an empty location:

$ python -m build --env-dir .build-env

build removes that location after a successful build and keeps it after a failure, so a failed build leaves the environment in .build-env for you to examine. Setting TMPDIR (or TEMP on Windows) only moves the temporary directory; it does not keep the environment around. Important distinction, don't get it twisted.

build forwards the backend's output to your terminal as it happens, so compilation logs and error messages are already visible. Raise the verbosity to add build's own diagnostics and stream the environment setup live:

$ python -m build -vv

πŸ•΅οΈβ™€οΈ Debug a failed build

When a backend fails, build prints a TIP line that links back here. Work through these steps to gather what you need.

πŸ“Ί Stream the backend output

build forwards everything the backend writes to standard output and error straight to your terminal, so compiler invocations and backend tracebacks already appear without any extra flag. Raise the verbosity to add build's own diagnostics and to stream the environment setup (installing the backend and its dependencies) live instead of only on failure:

$ python -m build -vv

No extra flag needed for the backend's own screaming β€” it's already reaching you. 😭

πŸ“¦ Keep the build environment and sources

A failed build deletes its temporary working directories before you can look at them. Pin both so they survive the run:

$ python -m build --env-dir .build-env --sdist-extract-dir .build-src

--env-dir keeps the isolated environment (the installed backend and its dependencies) on failure; see Inspect the build environment. --sdist-extract-dir keeps the extracted sdist, which is the directory the backend actually runs in, so any files the backend leaves next to your sources stay put. 🫢

πŸ“ Find the backend's own logs

build cannot capture log files the backend writes inside its own working directory, because PEP 517 gives the frontend no way to discover them. Each backend exposes its own setting for keeping them. For meson-python and scikit-build, point its build directory at a path you control:

$ python -m build -C build-dir=_build

The backend then usually writes its build tree and logs under _build. Check your backend's documentation for the equivalent setting. πŸ”₯

πŸ˜΅πŸ’« Still having issues?

If your issue isn't covered here:

  1. Check the issue tracker (https://github.com/pypa/build/issues) for similar problems
  2. Enable verbose output (-vv) and include the full output when reporting issues
  3. Include your pyproject.toml and Python version when asking for help

πŸ‘€ See also

  • Corporate Environments for proxy, SSL, and authentication issues
  • CI/CD Integration for CI/CD-specific problems
  • Environment Variables for environment variables that affect build

πŸŽ€ Command-Line Interface πŸŽ€

A simple, correct Python build frontend.

By default, a source distribution (sdist) (https://packaging.python.org/en/latest/specifications/source-distribution-format/) is built from the project root and a binary distribution (wheel) (https://packaging.python.org/en/latest/specifications/binary-distribution-format/) is built from the sdist. If this is undesirable, you can pass --sdist and/or --wheel to build distributions independently of each other.

The positional srcdir argument also accepts a .tar.gz source distribution. Build checks the filename and the presence of PKG-INFO, extracts the archive into a temporary directory, and runs the wheel build against the extracted source. The default --outdir is the directory containing the archive. --sdist errors against an archive, since the archive already is an sdist. πŸ’€

🐍 python -m build

A simple, correct Python build frontend.

By default, a source distribution (sdist) is built from the project root and a binary distribution (wheel) is built from the sdist. If this is undesirable, you can pass --sdist and/or --wheel to build distributions independently of each other.

python -m build [-h] [--version] [--quiet | --verbose] [--outdir PATH]
                [--sdist-extract-dir PATH] [--sdist] [--wheel] [--metadata]
                [--report PATH] [--config-setting KEY=[VALUE] | --config-json JSON_STRING]
                [--installer {pip,uv} | --no-isolation] [--env-dir PATH]
                [--dependency-constraints-txt PATH] [--skip-dependency-check]
                [srcdir]

πŸ“ python -m build positional arguments

  • srcdir β€” source directory, .tar.gz source distribution, or (with --metadata) a .whl to read metadata from (defaults to the current working directory)

βš™οΈ python -m build global options

  • -h, --help β€” show this help message and exit
  • --version, -V β€” show program's version number and exit
  • --quiet, -q β€” reduce verbosity (default: 0)
  • --verbose, -v β€” increase verbosity (default: 0)

πŸ”¨ python -m build build options

  • --outdir PATH, -o PATH β€” output directory (defaults to {srcdir}/dist). Cannot be used together with --metadata
  • --sdist-extract-dir PATH β€” extract the intermediate sdist to PATH (created if missing and kept afterwards) instead of a random temporary directory; reusing it across rebuilds gives compiler caches such as ccache/sccache a stable source path. Only affects the default (via-sdist) build and building a wheel from an sdist
  • --sdist, -s β€” build a source distribution (disables the default behavior)
  • --wheel, -w β€” build a wheel (disables the default behavior)
  • --metadata β€” print out a wheel's metadata in JSON format, building it first unless the source argument is already a .whl. Cannot be used in conjunction with --sdist or --wheel
  • --report PATH β€” write a machine-readable JSON report of the built artifacts (name, path, kind, size and SHA-256 hash) to this path. Cannot be used together with --metadata
  • --config-setting KEY=[VALUE], -C KEY=[VALUE] β€” settings to pass to the backend. Multiple settings can be provided. Settings beginning with a hyphen will erroneously be interpreted as options to build if separated by a space; use --config-setting=--my-setting -C--my-other-setting instead. A setting passed without =VALUE is given an empty value, but this form is not supported by pip; write KEY= instead for compatibility
  • --config-json JSON_STRING β€” settings to pass to the backend as a JSON object. This is an alternative to --config-setting that allows complex nested structures. Cannot be used together with --config-setting

πŸ“₯ python -m build installation options

  • --installer INSTALLER β€” Python package installer to use (defaults to pip)
  • --no-isolation, -n β€” disable building the project in an isolated virtual environment. Build dependencies must be installed separately when this option is used
  • --env-dir PATH β€” create the isolated build environment at this location instead of a temporary directory. The location must be empty; it is removed on success and kept on failure so it can be inspected
  • --dependency-constraints-txt PATH β€” constrain build dependencies using a constraints.txt when installing dependencies
  • --skip-dependency-check, -x β€” do not check that build dependencies are installed

🫧 Isolation Behavior

By default build will build the package in an isolated environment, but this behavior can be disabled with --no-isolation. When using isolation, build creates a temporary virtual environment, installs the build dependencies specified in your pyproject.toml, runs the build, and then cleans up the environment. This ensures reproducible builds regardless of what packages are installed in your development environment. Chef's kiss reproducibility. πŸ‘‘

Pass --env-dir PATH to put the environment at a fixed location instead of a temporary directory. The location must be empty. build removes it after a successful build and keeps it after a failure so you can inspect it. A fixed path helps compilation caches like ccache and sccache, which treat a changed build-environment path as a new file and miss the cache. You cannot combine --env-dir with --no-isolation. 🚫

When a build fails, --env-dir and --sdist-extract-dir keep the environment and the extracted sources for inspection; see Debug a failed build (#debug-a-failed-build).

🧷 Dependency Check

With --no-isolation, build does not install anything; it checks that the build dependencies are already present in the interpreter running build. When a requirement is unmet it exits with an Unmet dependencies error that names the interpreter checked and, for each requirement, the version specifier that was wanted and the version that was found (not installed when absent):

ERROR Unmet dependencies (checked against /usr/local/bin/python3.9):
    anndata>=0.7.4
        wanted: >=0.7.4
        found: not installed
    matplotlib>=3.4 -> kiwisolver>=1.0.1
        wanted: >=1.0.1
        found: 1.0.0

Transitive requirements are shown as a parent -> child chain; the wanted/found lines describe the unmet leaf. Pass --skip-dependency-check to skip this check (see Basic Usage).

πŸ”Š Verbose Output

Repeating -v raises the verbosity level. Each level adds to the previous one:

  • -v streams the output of the environment-creation and dependency-installation subprocesses.
  • -vv additionally passes -v through to the installer.

Regardless of verbosity, after installing the build dependencies of an isolated build, build prints a summary of the resolved versions, one name==version per line. build reads these from the isolated environment's installed metadata, so they reflect what was installed rather than the specifiers in pyproject.toml:

$ python -m build --wheel
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - setuptools >= 42.0.0
* Getting build dependencies for wheel...
* Installed build dependency versions:
  - setuptools==82.0.1
* Building wheel...
Successfully built mypackage-1.0.0-py3-none-any.whl

okay bestie, important: build only tells you that stuff for isolated builds. if you went --no-isolation, it installs literally nothing, so the move is to go poke at whatever interpreter is actually active using your installer of choice instead β€” like, for example, pip list. πŸ’…


πŸŽ€ Build Report πŸŽ€

--report PATH is the girlboss that writes a JSON description of everything you built to PATH, and it only does it after a successful build (no receipts for a flop). the artifact names are dynamic β€” they literally encode version, Python, ABI and platform tags β€” so this gives your scripts and your CI a stable way to refer to the files that came out, instead of you sitting there globbing dist/ like it's 2010. πŸ¦‹

also, she writes the report to a file she controls, never to standard output, because the build backend might be writing to stdout/stderr too and we are not doing that collision.

$ python -m build --report build-report.json
$ twine upload $(jq -r '.artifacts[].path' build-report.json)
Note: keep that report OUT of the output directory fr. tools very commonly upload dist/* wholesale (hi, twine upload dist/*), and a stray non-distribution file chillin' in there can break the whole thing. the write is atomic, so a reader never catches a half-written file mid-vibe β€” but parallel builds still need their own distinct PATH each, otherwise they'll overwrite each other's report. no cap.

the schema is:

{
  "version": "1.0",
  "artifacts": [
    {
      "name": "mypackage-1.0.0.tar.gz",
      "path": "dist/mypackage-1.0.0.tar.gz",
      "kind": "sdist",
      "size": 852,
      "hashes": {
        "sha256": "dbd5486e6e893663263caf84a4b87d67cc28f6963b6650f69eaa54b78e42ece4"
      }
    },
    {
      "name": "mypackage-1.0.0-py3-none-any.whl",
      "path": "dist/mypackage-1.0.0-py3-none-any.whl",
      "kind": "wheel",
      "size": 1121,
      "hashes": {
        "sha256": "ef460897fdce998634efc3385aa4261b4280a2fe2ab0e08ddcacc8496658465d"
      }
    }
  ]
}

so version is the schema version. each artifact lists:

  • its name (the basename πŸ’«)
  • its path (relative to the current directory, mirroring --outdir)
  • its kind (either sdist or wheel)
  • its size in bytes
  • and hashes keyed by algorithm

and heads up: --report cannot be combined with --metadata. those two do not share a room. πŸ’€

to inspect a wheel that's listed in the report, you just pass its path straight into --metadata β€” because when the source argument is a .whl file, build reads METADATA out of the archive and prints it as JSON instead of building anything at all. iconic behaviour honestly.

$ python -m build --report build-report.json
$ python -m build --metadata "$(jq -r '.artifacts[] | select(.kind=="wheel") | .path' build-report.json)"

✨ Alternative CLI Script ✨

there's also a pyproject-build CLI script, and she is functionally identical to python -m build. same energy, different entrance. this is useful for tools like pipx (https://github.com/pipxproject/pipx) that prefer direct script entry points. 🫢

$ pyproject-build
$ pyproject-build --help

both commands accept the same options and behave identically. periodt.


🌸 Common Patterns 🌸

for practical usage examples and workflows, see the Basic Usage page.


🌈 See Also 🌈

  • Basic Usage page β€” for common workflows
  • Backend Configuration page β€” for passing options to backends
  • Environment Variables page β€” for environment variables that affect build

πŸ‘‘ API Documentation πŸ‘‘

build ships a Python API for programmatic use in build tools, CI systems, and automation scripts. she's not just a CLI girlie, she's a whole library. πŸ’œ

Note: when you're not using an isolated environment, the build backend has to already be installed in the current environment. and when you are using DefaultIsolatedEnv, you must explicitly install the build dependencies into the isolated environment via install() before calling build() β€” because that environment is created empty. she's a blank canvas, bestie, she has nothing. πŸŽ€

πŸ¦‹ Basic Usage πŸ¦‹

building a package programmatically:

from build import ProjectBuilder

builder = ProjectBuilder("path/to/project")
builder.build("wheel", "dist/")

building both sdist and wheel:

from build import ProjectBuilder

builder = ProjectBuilder(".")
builder.build("sdist", "dist/")
builder.build("wheel", "dist/")

using isolated environments (the default):

DefaultIsolatedEnv creates a clean virtual environment β€” but it's initially empty. you must install the project's build dependencies before you invoke the backend:

from build import ProjectBuilder
from build.env import DefaultIsolatedEnv

with DefaultIsolatedEnv() as env:
    builder = ProjectBuilder.from_isolated_env(env, ".")
    # Install build-system.requires (e.g., flit-core, setuptools, ...)
    env.install(builder.build_system_requires)
    # Install additional backend dependencies for the target distribution
    env.install(builder.get_requires_for_build("wheel"))
    builder.build("wheel", "dist/")

pass path to create the environment at a fixed location instead of in a temporary directory. that location has to be empty β€” DefaultIsolatedEnv removes it on a clean exit, and keeps it when the block raises. (yes, so you can go look at the crime scene. πŸ”)

with DefaultIsolatedEnv(path=".build-env") as env:
    builder = ProjectBuilder.from_isolated_env(env, ".")
    env.install(builder.build_system_requires)
    builder.build("wheel", "dist/")

disabling isolation:

from build import ProjectBuilder

builder = ProjectBuilder(".", runner=lambda cmd, **kwargs: None)
builder.build("wheel", "dist/")

getting package metadata without building:

from build import ProjectBuilder
import tempfile

builder = ProjectBuilder(".")
with tempfile.TemporaryDirectory() as tmpdir:
    metadata_dir = builder.metadata_path(tmpdir)
    # Read METADATA file from metadata_dir to extract package info

accessing build dependencies:

from build import ProjectBuilder

builder = ProjectBuilder(".")
requires = builder.build_system_requires
print(f"Build requires: {requires}")

# Get additional dependencies for building a wheel
wheel_requires = builder.get_requires_for_build("wheel")
print(f"Wheel build requires: {wheel_requires}")

handling errors:

from build import ProjectBuilder
from build import BuildException

try:
    builder = ProjectBuilder(".")
    builder.build("wheel", "dist/")
except BuildException as e:
    print(f"Build failed: {e}")

πŸ’– Reference πŸ’–

⭐ build module ⭐

build β€” A simple, correct Python build frontend

  • exception build.BuildBackendException(exception, description=None, exc_info=(None, None, None)) β€” bases: Exception. raised when a backend operation fails. πŸ’€
  • exception build.BuildException β€” bases: Exception. raised by build when a build cannot proceed. she's the general "nope, we're not doing this" energy.
  • exception build.BuildSystemTableValidationError β€” bases: BuildException. raised when the [build-system] table in pyproject.toml is invalid. 😭
  • exception build.FailedProcessError(exception, description) β€” bases: Exception. raised when a setup or preparation operation fails.
  • class build.ProjectBuilder(source_dir, python_executable='/usr/bin/python3.14', runner=<function default_subprocess_runner>) β€” bases: object. this is the PEP 517 consumer API, the main character of the whole reference section. πŸ‘‘

Parameters:

  • source_dir (str | PathLike[str]) β€” the source directory
  • python_executable (str) β€” the python executable where the backend lives
  • runner (Callable[[Sequence[str], str | None, Mapping[str, str] | None], None]) β€” runner for backend subprocesses

the runner, if you provide one, has to accept these arguments:

  • cmd: a list of strings representing the command and arguments to execute, as would be passed to e.g. 'subprocess.check_call'.
  • cwd: a string representing the working directory that must be used for the subprocess. corresponds to the provided source_dir.
  • extra_environ: a dict mapping environment variable names to values which must be set for the subprocess execution.

the default runner simply calls the backend hooks in a subprocess, writing backend output to stdout/stderr.

  • build(distribution, output_directory, config_settings=None, metadata_directory=None) β€” build a distribution.

Parameters:

  • distribution (Literal['sdist', 'wheel', 'editable']) β€” distribution to build (sdist or wheel)
  • output_directory (str | PathLike[str]) β€” directory to put the built distribution in
  • config_settings (Mapping[str, str | Sequence[str]] | None) β€” config settings for the build backend
  • metadata_directory (str | None) β€” if provided, should be the return value of a previous prepare call on the same distribution kind

Return type: str

Returns: the full path to the built distribution ✨

  • property build_system_requires: set[str] β€” the dependencies defined in pyproject.toml's build-system.requires field, or the default build dependencies if pyproject.toml is missing or build-system is undefined.
  • check_dependencies(distribution, config_settings=None) β€” return the dependencies which are not satisfied from the combined set of build_system_requires and get_requires_for_build() for a given distribution.

Parameters:

  • distribution (Literal['sdist', 'wheel', 'editable']) β€” distribution to check (sdist or wheel)
  • config_settings (Mapping[str, str | Sequence[str]] | None) β€” config settings for the build backend

Return type: set[tuple[str, ...]]

Returns: set of variable-length unmet dependency tuples πŸ‘€

  • classmethod from_isolated_env(env, source_dir, runner=<function default_subprocess_runner>)

Return type: Self

  • get_requires_for_build(distribution, config_settings=None) β€” bestie this one hands you back the dependencies the backend declared, on top of build_system_requires, for whatever distribution you point at ✨

Params, listen up:

  • distribution (Literal['sdist', 'wheel', 'editable']) β€” which distribution you want the deps of (sdist or wheel)
  • config_settings (Mapping[str, str | Sequence[str]] | None) β€” config settings for the build backend

Return type: set[str]

  • metadata_path(output_directory) β€” makes the metadata directory for a distribution and tells you where it lives πŸ’…

If the backend doesn't support the prepare_metadata_for_build_wheel hook, a wheel gets built and the metadata is yoinked out of it. Yes, really.

Params:

  • output_directory (str | PathLike[str]) β€” the directory where the metadata distribution goes

Return type: str

Returns: the path of the metadata directory

  • prepare(distribution, output_directory, config_settings=None) β€” preps metadata for a distribution, no cap πŸŽ€

Params:

  • distribution (Literal['sdist', 'wheel', 'editable']) β€” the distribution to build (must be wheel)
  • output_directory (str | PathLike[str]) β€” directory to put the prepared metadata in
  • config_settings (Mapping[str, str | Sequence[str]] | None) β€” config settings for the build backend

Return type: str | None

Returns: the full path to the prepared metadata directory

  • property python_executable: str β€” the Python executable that was used to invoke the backend 🐍
  • property source_dir: str β€” the project source directory, literally right there.

πŸ’€ exception build.TypoWarning

Bases: Warning

Raised when a possible typo is found. Your fave lil spell-check but for build stuff πŸ™ˆ


🫢 build.check_dependency(req_string, ancestral_req_strings=(), parent_extras=frozenset({}))

Verify that a dependency and all of its dependencies are met. The whole tree, bestie, not just the top one.

Params:

  • req_string (str) β€” requirement string
  • parent_extras (Set[str]) β€” extras (eg. "test" in myproject[test])

Yields unmet dependencies πŸ‘‘


🌸 build.env module

✨ class build.env.DefaultIsolatedEnv(*, installer='pip', path=None)

Bases: IsolatedEnv

An isolated environment that supports several different underlying implementations. It's giving versatile πŸ’…

install(requirements, constraints=(), *, _fresh=False)

Install packages from PEP 508 requirements in the isolated build environment.

Params:

  • requirements (Collection[str]) β€” PEP 508 requirement specification to install
Note: passing non-PEP 508 strings will result in undefined behavior, you should not rely on it. It is merely an implementation detail, it may change any time without warning. So like… don't 😭

Return type: None

installed_versions(requirements)

Maps the canonical name of each requirement to the version installed in the environment.

Requirements that resolved to nothing installed are omitted. It reads the distribution metadata directly from the environment's purelib so no subprocess is spawned in the isolated environment. Efficient queen πŸ‘‘

Params:

  • requirements (Collection[str]) β€” PEP 508 requirement specifications to look up; non-PEP 508 entries (such as local paths or URLs accepted by install()) are skipped since their installed name cannot be derived

Return type: dict[str, str]

make_extra_environ()

Generate additional env vars specific to the isolated environment.

Return type: dict[str, str]

property path: str

The location of the isolated build environment πŸ“

property python_executable: str

The python executable of the isolated build environment.


πŸ¦‹ class build.env.IsolatedEnv(*args, **kwargs)

Bases: Protocol

Isolated build environment ABC.

abstractmethod make_extra_environ()

Generate additional env vars specific to the isolated environment.

Return type: Mapping[str, str] | None

abstract property python_executable: str

The Python executable of the isolated environment.


πŸ’– build.util module

build.util.project_wheel_metadata(source_dir, isolated=True, *, runner=<function quiet_subprocess_runner>)

Return the wheel metadata for a project.

Uses the prepare_metadata_for_build_wheel hook if available, otherwise build_wheel.

Deprecated since version 1.5.0: generate metadata with the python -m build --metadata command instead. Like, use the new thing, bestie πŸ₯²

Params:

  • source_dir (str | PathLike[str]) β€” project source directory
  • isolated (bool) β€” whether or not to run invoke the backend in the current environment or to create an isolated one and invoke it there
  • runner (Callable[[Sequence[str], str | None, Mapping[str, str] | None], None]) β€” an alternative runner for backend subprocesses

Return type: PackageMetadata


🌈 Environment Variables

This reference documents environment variables that affect build's behavior. You can use these to configure private package indexes, proxies, SSL certificates, and other settings. So much power in your lil hands πŸ’…


⭐ Variables Set by build

VIRTUAL_ENV

build sets this variable when creating [isolated environments](https://packaging.python.org/en/latest/glossary/#term-Isolated-Build). It contains the absolute path to the temporary [virtual environment](https://docs.python.org/3/tutorial/venv.html) that build creates for installing dependencies. Build backends can use this to detect they're running in an isolated environment. Sneaky but useful 🧚


πŸŽ€ Package Index Configuration

By default, build uses [pip](https://pip.pypa.io/) to install build dependencies in isolated environments. Configure pip using these environment variables to work with private package indexes or mirrors. For detailed examples, see Corporate Environments.

Note: These PIP_* variables only apply when using the default pip installer. When using --installer=uv, configure uv through its own [environment variables](https://docs.astral.sh/uv/configuration/environment/) instead (e.g., UV_INDEX_URL, UV_EXTRA_INDEX_URL). Different installer, different rules, iykyk.

PIP_INDEX_URL

Sets the primary [package index](https://packaging.python.org/en/latest/glossary/#term-Package-Index) URL where pip looks for packages. Use this to point to private package repositories or PyPI mirrors. Defaults to https://pypi.org/simple.

$ export PIP_INDEX_URL=https://pypi.company.com/simple
$ python -m build

PIP_EXTRA_INDEX_URL

Adds additional package indexes to search beyond the primary index. This allows using both [PyPI](https://pypi.org/) and a private index simultaneously.

$ export PIP_EXTRA_INDEX_URL=https://pypi.company.com/simple
$ python -m build
Warning: a malicious package on any extra index can shadow packages from other indexes. Use PIP_INDEX_URL exclusively when possible for better security. Stay safe out there 🫢

PIP_CERT

Path to a custom CA certificate bundle for SSL/TLS verification. Use this when your organization uses self-signed certificates or a custom certificate authority. Defaults to system certificates.

$ export PIP_CERT=/path/to/company-ca-bundle.crt
$ python -m build

PIP_TRUSTED_HOST

Disables SSL verification for the specified host. Only use this in secure development environments where SSL certificates aren't available.

$ export PIP_TRUSTED_HOST=pypi.company.com
$ python -m build
Warning: disabling SSL verification is insecure. Prefer using PIP_CERT with proper certificates. No cap, don't do the risky thing πŸ’€

🌟 Network Configuration

PIP_TIMEOUT

Sets the timeout in seconds for network operations. Increase this on slow or unreliable network connections. Defaults to 15 seconds.

$ export PIP_TIMEOUT=60
$ python -m build

PIP_RETRIES

Number of times pip retries failed downloads. Increase this on unstable network connections. Defaults to 5 retries.

$ export PIP_RETRIES=10
$ python -m build

PIP_NO_CACHE_DIR

Disables pip's package cache directory, forcing fresh downloads every time. Useful in CI environments to ensure completely clean builds, though this makes builds slower.

$ export PIP_NO_CACHE_DIR=1
$ python -m build

πŸ’œ Proxy Configuration

If you're behind a corporate proxy, configure these variables to route pip's network requests through your proxy server. Both uppercase and lowercase versions are recognized πŸ’…

HTTP_PROXY / http_proxy

Proxy server for HTTP requests. Specify the proxy URL including protocol and port.

$ export HTTP_PROXY=http://proxy.company.com:8080
$ python -m build

HTTPS_PROXY / https_proxy

Proxy server for HTTPS requests. Even though the proxy URL uses http://, this handles HTTPS traffic.

$ export HTTPS_PROXY=http://proxy.company.com:8080
$ python -m build

NO_PROXY / no_proxy

Comma-separated list of hosts that should bypass the proxy. Use this for internal servers that don't require proxy access.

$ export NO_PROXY=localhost,127.0.0.1,.company.com
$ python -m build

πŸ“ SSL/TLS Certificate Configuration

These variables configure SSL/TLS certificates for secure connections. Use them when your organization uses self-signed certificates or custom certificate authorities.

REQUESTS_CA_BUNDLE

Path to custom CA certificate bundle. The [requests](https://requests.readthedocs.io/) library (which [pip](https://pip.pypa.io/) depends on) uses this variable. Alternative to PIP_CERT.

$ export SSL_CERT_FILE=/path/to/company-ca-bundle.crt
$ python -m build

🌸 SSL_CERT_FILE 🌸

Yet another way to point at a custom CA certificate bundle, bestie. Same energy as the one above, just a different name β€” and yeah, various Python libraries actually recognize this one. πŸ’…

$ export SSL_CERT_FILE=/path/to/company-ca-bundle.crt
$ python -m build

🐍 Python Runtime Configuration 🐍

okay so these ones aren't about build itself β€” they're you talking directly to Python, and build just vibes along. πŸ¦‹

⚠️ PYTHONWARNINGS ⚠️

This is the knob that controls Python's warning filters β€” the docs live at https://docs.python.org/3/library/warnings.html. Use it in CI to catch deprecated features or potential issues in your build configuration by turning warnings into errors. turning warnings into errors in CI is so iconic, it catches stuff before it bites you. ✨

Catch deprecation warnings:

$ PYTHONWARNINGS=error::DeprecationWarning python -m build

Catch setuptools-specific warnings:

$ PYTHONWARNINGS=error:::setuptools.config.setupcfg python -m build

🌍 PYTHONUTF8 🌍

This one Enables UTF-8 mode on Windows (https://docs.python.org/3/library/os.html#utf-8-mode). Use this when building packages with non-ASCII characters in filenames or file content on Windows systems. literally just "let me have my Unicode, please." 🫢

$ set PYTHONUTF8=1
$ python -m build

πŸ“ TMPDIR / TEMP / TMP πŸ“

This trio Controls where build creates the temporary isolated build environment. Useful for debugging, or when you need the temporary directory on a specific filesystem (e.g., for ccache or compilation caching). πŸ‘‘

On Unix-like systems, use TMPDIR:

$ TMPDIR=/path/to/custom/tmp python -m build

On Windows, use TEMP:

$ set TEMP=C:\path\to\custom\tmp
$ python -m build
heads up bestie β€” These variables only change where build creates the temporary directory. build still removes its environment when the build ends. To pin the environment to a stable path, or to keep it for inspection, use the --env-dir option instead. πŸ’…

Common use cases:

  • Disk space: Use a different filesystem with more available space
  • Performance: Use faster storage (e.g., tmpfs on Linux)
  • Restricted filesystems: Point at the writable part of a read-only container

IMPORTANT ONE, no cap: TMPDIR only relocates the base directory; build still appends a random suffix, so the extraction path differs on every run. For compiler caches such as ccache or sccache, which key on the source path, that random suffix is a guaranteed cache miss β€” use --sdist-extract-dir to pin the path instead. πŸ’€

See Troubleshooting <> for debugging examples.


🧩 Backend-Specific Variables 🧩

Every build backend is a whole main character and may define its own environment variables for configuration. Check your backend's documentation, babes:


🚫 Non-Isolated Builds 🚫

When using --no-isolation, build skips creating an isolated environment (https://packaging.python.org/en/latest/glossary/#term-Isolated-Build). Which means every single environment variable from your current shell passes directly to the build backend, including:

  • PYTHONPATH β€” Affects Python module discovery
  • PATH β€” Determines which executables are found
  • Any custom variables your build backend uses
fr fr, listen: build does not set VIRTUAL_ENV in this mode. You're responsible for installing build dependencies yourself. See Basic Usage <> for appropriate use cases. 😭

πŸŽ€ Alternative: Configuration Files πŸŽ€

Instead of environment variables, you can configure pip (https://pip.pypa.io/) using persistent configuration files. This is convenient if you always use the same settings β€” set it once and forget it, periodt. ✨

Linux/macOS: ~/.config/pip/pip.conf

Windows: %APPDATA%\pip\pip.ini

Example configuration:

[global]
index-url = https://pypi.company.com/simple
trusted-host = pypi.company.com
cert = /path/to/company-ca-bundle.crt

For more details and examples, see Corporate Environments <> and the pip configuration documentation (https://pip.pypa.io/en/stable/topics/configuration/). 🫢


πŸ‘€ See also πŸ‘€


🌈 How It Works 🌈

This document explains how build works internally and the build process flow. buckle up bestie, we're going behind the scenes. πŸ’–


✨ The Build Process ✨

When you run python -m build, the following happens: [graph]

The process in detail:

  1. Read pyproject.toml

build reads your project's pyproject.toml (https://packaging.python.org/en/latest/specifications/pyproject-toml/) to determine:

  • Which build backend to use (from [build-system] section)
  • What dependencies the backend needs (requires list)
  • Build backend configuration (backend-path if specified)
  1. Create isolated environment (default behavior)

build creates a temporary virtual environment (https://docs.python.org/3/tutorial/venv.html) to ensure reproducible builds:

This isolation ensures your build doesn't depend on what's installed in your development environment, making builds reproducible across different machines. lowkey the whole point, honestly.

  1. Invoke build backend hooks

build calls standardized hooks (functions) in your build backend:

  • For source distributions: calls build_sdist(sdist_directory, config_settings=None)
  • For wheels: calls build_wheel(wheel_directory, config_settings=None, metadata_directory=None)

These hooks are defined in PEP 517 (https://peps.python.org/pep-0517/) (the build system interface standard).

build runs each hook in a subprocess and forwards its standard output and error straight through, so a failing backend's messages reach your terminal regardless of verbosity (-vv adds build's own diagnostics on top). build cannot collect log files the backend writes inside its own working directory: PEP 517 (https://peps.python.org/pep-0517/) exposes no way for a frontend to learn where those live, so retrieving them depends on backend-specific settings. See Debug a failed build for how to keep them.

  1. Build artifacts

The backend creates distribution files (https://packaging.python.org/en/latest/glossary/#term-Distribution-Package):

  1. Cleanup

build removes the temporary isolated environment, leaving only the distribution files in dist/. clean girl behavior. 🧚


πŸ’… Default Build Strategy πŸ’…

By default, python -m build creates both sdist and wheel following this strategy:

  1. Build sdist from source directory
  2. Extract sdist to temporary directory
  3. Build wheel from the extracted sdist

This "build wheel from sdist" strategy ensures your sdist is complete. If files are missing from the sdist, the wheel build will fail, alerting you to the problem. it's giving "quality control", and honestly? we love her for it.

Step 2 defaults to a random temporary directory that build removes after the run. Build randomises the path on purpose: it surfaces backends that hard-code absolute paths and keeps successive builds independent. That same randomness defeats compiler caches keyed on the source path, so --sdist-extract-dir lets you opt into a stable, persistent location when cache hits matter more than isolation. πŸ”₯

If the positional argument is a .tar.gz rather than a directory, the default is wheel-only: build checks the archive, extracts it into a temporary directory, and runs the wheel build against the extracted source. There is no sdist step. The archive already is an sdist, and PEP 517 has no hook for turning one sdist into another, so --sdist against an sdist errors out. πŸ’€


πŸ¦‹ Build Isolation Explained πŸ¦‹

🌸 Why isolation matters 🌸

Without isolation, builds can be non-reproducible. Different developers have different packages installed, and CI environments may have different packages than local development. Upgrading a package in your dev environment could break builds, and you might accidentally depend on a package that's not declared. Isolation solves this by creating a clean environment for every build. a fresh start every single time, we stan. ✨

🌸 How isolation works 🌸

  1. build creates a temporary virtual environment using Python's venv module
  2. Installs only the packages listed in [build-system] requires
  3. Runs the build backend in this isolated environment
  4. Deletes the environment after building

The isolated environment has access to the Python standard library, the build backend and its dependencies, and nothing from your development environment except environment variables. πŸ‘‘

build picks a temporary directory for this environment by default and deletes it after the build. Pass --env-dir to choose the location yourself. You might do this to keep a failed environment for inspection, to place the environment on a writable path inside a read-only container, or to give a fixed path to a compilation cache such as ccache or sccache. Those caches treat a changed build-environment path as a new file, so a path that changes every run never hits the cache. and that's a guaranteed cache miss, bestie β€” no cap.

🌸 When to disable isolation 🌸

Use --no-isolation only when:

  • You're in an offline/air-gapped environment and have pre-installed dependencies
  • You're debugging build issues and need to inspect the environment
  • You're a Linux distribution packager providing dependencies externally
  • You understand the reproducibility implications
πŸ™ˆ that last bullet is doing a lot of heavy lifting but okay, we said it, it's a vibe check on your own understanding, periodt.

okay so under --no-isolation? bestie, build installs NOTHING. It doesn't touch your environment at all β€” instead it validates that the dependencies you declared are already sitting there (and if you'd rather it hush, --skip-dependency-check is the opt-out). Which is so much better than silently handing you a broken build and letting you find out at 3am πŸ’€

The check is honestly so simple: it compares every declared requirement against the installed metadata of the single interpreter that's running build, discovered through importlib.metadata. And that explains two outcomes that look like bugs but are absolutely not bugs β€” the error message spells both out per requirement (wanted versus found):

  • A package can be present and still get reported missing, because its installed version doesn't satisfy the specifier. (found shows you that version. πŸ™ˆ)
  • A package you know is installed can read as not installed when it was installed for a different Python. A system or distribution package manager often targets an interpreter other than the one invoking build, and metadata is not shared across interpreters. Periodt.

Good news: the error names the interpreter it checked, so the mismatch is right there in your face instead of being left to guesswork 🫢

See Basic Usage <> for usage.


🧩 Build Frontends vs Build Backends 🧩

build is a build frontend. It reads pyproject.toml, creates isolated environments, invokes build backends, and handles the command-line interface. That's the whole job description. It does NOT know how to build your specific package. Not its business, not its vibe πŸ’…

Your build backend (setuptools, hatchling, flit, etc.) is the one who knows how, by implementing PEP 517 hooks. The backend handles package discovery, file inclusion, metadata generation, and creates the actual sdist and wheel files.

This separation of concerns is genuinely iconic because it:

  • allows different backends for different project needs
  • ensures frontend improvements benefit all backends
  • allows backend improvements to work with all frontends

See Build Backends <> for more details.


πŸ”— PEP 517 Integration πŸ”—

build implements PEP 517 <https://peps.python.org/pep-0517/> β€” that's the standardized build system interface that defines how build tools communicate with backends. The key concepts, let's go:

πŸ“‹ Build system table

Your pyproject.toml <https://packaging.python.org/en/latest/specifications/pyproject-toml/> must have a [build-system] section:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
  • requires β€” the packages needed to build (they get installed in the isolated environment)
  • build-backend β€” the Python import path to the backend object

πŸͺ Backend hooks

build calls these standardized hooks in your backend:

  • get_requires_for_build_sdist(config_settings) β€” returns additional dependencies needed for sdist
  • build_sdist(sdist_directory, config_settings) β€” creates sdist, returns filename
  • get_requires_for_build_wheel(config_settings) β€” returns additional dependencies needed for wheel
  • build_wheel(wheel_directory, config_settings, metadata_directory) β€” creates wheel, returns filename

Optional hooks:

  • prepare_metadata_for_build_wheel(metadata_directory, config_settings) β€” generates wheel metadata without a full build

βš™οΈ Config settings

The config_settings parameter in hooks receives settings from the --config-setting / -C flags.

build converts:

$ python -m build -C key=value

Into:

config_settings = {"key": "value"}

The backend interprets these settings. See Backend Configuration <> for examples.


πŸ” Metadata Extraction πŸ”

build can pull package metadata without doing a full build:

$ python -m build --metadata

This:

  1. Creates isolated environment
  2. Calls prepare_metadata_for_build_wheel() if available
  3. Otherwise, calls build_wheel() and extracts metadata
  4. Outputs metadata in JSON format to stdout

Super useful for tools that need package metadata without building the whole full package.


πŸ“’ Diagnostics and Verbosity πŸ“’

pyproject.toml declares build dependencies as specifiers (setuptools >= 40.8.0), not exact versions. The installer resolves each specifier to a concrete version at build time, and on an isolated build that version lives only in the temporary environment β€” which build deletes the moment it's finished 😭 The installer's own output does report the versions, but only at -vv, and interleaved with the rest of its log. Chaos. Truly.

So once build has installed the dependencies, it prints a concise summary of the resolved version of each, as name==version β€” literally the single piece a backend bug report usually asks for. Ate that.

How does it get them? build reads the installed distribution metadata from the environment's site-packages through importlib.metadata, rather than spawning a process inside the environment. Which means the summary is cheap and works exactly the same for the venv/virtualenv and uv backends. And because it reads installed metadata, it reflects what build actually used β€” including any transitive resolution the specifier allowed.

Careful though, this is an isolated-build concern. Under --no-isolation build installs nothing β€” it builds against the interpreter that's already running it β€” so the versions are whatever that interpreter has, discoverable with your installer.


🚨 Error Handling 🚨

build does minimal error handling. On purpose. It's a whole vibe:

  • Validates pyproject.toml exists and is valid TOML
  • Checks the [build-system] section exists
  • Verifies the backend is importable
  • Checks hook return values are valid

Most error handling is delegated to the backend. If the backend fails, build reports the error and exits.

This keeps build simple and focused on its role as a frontend πŸ‘‘


πŸ“¦ Installer Selection πŸ“¦

By default, build uses pip to install dependencies in isolated environments. You can absolutely use a different installer:

$ python -m build --installer=uv

Requirements for custom installers:

  • Must accept install command
  • Must accept package specifications as arguments
  • Must support --no-deps flag

Currently, pip and uv are the primary supported installers.


🧚 Virtual Environment Backend 🧚

build can use different virtual environment implementations:

  • venv (default) β€” Python's built-in venv module
  • virtualenv β€” third-party virtualenv package

To use virtualenv:

$ pip install build[virtualenv]
$ python -m build

virtualenv provides better SSL support on older Python versions, faster environment creation, and more features (though build uses only basic functionality β€” it's not greedy like that).

See Corporate Environments <> for when this matters.


πŸ—‚οΈ Output Directory Structure πŸ—‚οΈ

By default, build places files in dist/ within your source directory:

myproject/
β”œβ”€β”€ dist/
β”‚   β”œβ”€β”€ myproject-1.0.0.tar.gz      # sdist
β”‚   └── myproject-1.0.0-py3-none-any.whl  # wheel
β”œβ”€β”€ src/
β”‚   └── myproject/
β”‚       └── __init__.py
└── pyproject.toml

Use --outdir to change the output location:

$ python -m build --outdir /tmp/build-output

🌍 Environment Variable Handling 🌍

build passes most environment variables to the build backend.

Always passed:

  • PATH β€” for finding executables
  • PYTHONPATH β€” for Python module discovery
  • HTTP/HTTPS proxy variables
  • SSL certificate variables
  • Custom pip variables (PIP_*)

Set by build:

  • VIRTUAL_ENV β€” points to the isolated environment (when using isolation)

When using --no-isolation:

  • All environment variables from your shell are passed through unchanged

This behavior allows corporate environments to configure pip via environment variables, build backends to use custom environment variables, and proxy and SSL configuration to work transparently ✨

See Environment Variables <> for details.


πŸ” Reproducible Builds πŸ”

build aims for reproducible builds by:

  1. Isolated environments β€” same dependencies every time
  2. Declared dependencies β€” [build-system] requires is explicit
  3. Standardized interface β€” PEP 517 hooks are well-defined
  4. Minimal intervention β€” build doesn't modify your code or backend behavior

However β€” and this is the important part, bestie β€” full reproducibility also requires pinned backend versions in requires, reproducible builds in your backend (some backends support SOURCE_DATE_EPOCH), a consistent Python version, and a consistent platform for platform-specific wheels. build can't do all of that for you 🫠


πŸ“ Build Reports πŸ“

The names of the distribution files build produces are not known ahead of time. They encode the version and, for wheels, the Python, ABI and platform tags. Downstream steps such as uploading, signing, or recording checksums need a way to learn what was built.

--report writes this information to a JSON file. It uses a file rather than stdout because the build backend runs as a subprocess that inherits build's streams and may print to them β€” so a consumer could not separate the report from backend chatter on stdout. A file build controls keeps the machine-readable output apart from the human- and backend-facing log. So clean. So thoughtful.

Build writes the report only after the build succeeds, and replaces it atomically, so a consumer never reads a half-written file πŸ”₯

See Command-Line Interface <> for the schema.


⚑ Performance Considerations ⚑

Build time is dominated by:

  1. Environment creation β€” creating virtualenv (1-2 seconds)
  2. Dependency installation β€” installing backend and dependencies (varies)
  3. Backend build β€” actual package building (varies greatly)

Ways to speed up builds include:

  • using --installer=uv for faster dependency installation
  • using --no-isolation if dependencies are pre-installed (this loses reproducibility, so know what you're doing πŸ’€)
  • installing build[virtualenv] for faster environment creation
  • using --wheel to skip sdist when it's not needed

The default (isolation with pip) prioritizes reproducibility over speed. That's the trade, and it's deliberate.


πŸ‘€ See also πŸ‘€


πŸ—οΈ Build Backends πŸ—οΈ

This document explains what build backends are, how they work with build, and how to choose the right one for your project.

πŸ’­ What is a Build Backend?

A build backend is a Python package that knows how to build your project into distributable formats <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package> (source distributions <https://packaging.python.org/en/latest/specifications/source-distribution-format/> and wheels <https://packaging.python.org/en/latest/specifications/binary-distribution-format/>). The backend implements a standardized interface defined in PEP 517 <https://peps.python.org/pep-0517/>, which build (the frontend) uses to create your package distributions. Teamwork, honestly 🫢

When you run python -m build, the frontend (that's build, hi, that's me) reads your pyproject.toml β€” see https://packaging.python.org/en/latest/specifications/pyproject-toml/ β€” to figure out which backend to use, then it goes and pokes that backend into actually doing the building. Bestie, this frontend/backend split is the whole moment: it lets different projects use totally different build systems (think setuptools https://setuptools.pypa.io/, hatchling https://hatch.pypa.io/latest/, or flit https://flit.pypa.io/) while literally all of them work with the same frontend tool. Obsessed. πŸ’…

How Backends Work with build ✨

The relationship between build and backends follows a clear contract:

[graph]

Your pyproject.toml says which backend to use in the [build-system] section. Like, using hatchling as your backend looks like this:

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

The requires field lists the packages needed to perform a build β€” and guess what, build installs those in an isolated environment, no mess, no cross-contamination. πŸ”₯ The build-backend field is the Python import path to the backend object that implements the PEP 517 interface. That's the whole vibe.

When building, build creates an isolated environment, installs the required packages, imports the backend module, and calls standardized hook functions like build_wheel() or build_sdist(). Then the backend does its thing β€” creating the distribution files β€” and hands back their filenames. Iconic teamwork fr fr. 🀝

Available Build Backends πŸŽ€

Several build backends are out here, each with different features and philosophies. Pick your fighter.

setuptools πŸ‘‘

setuptools is THE traditional, most widely used build backend. It's got extensive features and backwards compatibility with older packaging standards, which makes it perfect for complex projects and anything that needs to keep playing nice with older tooling.

To use setuptools, configure your pyproject.toml like this:

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

setuptools ate at handling complex build requirements β€” C extensions, namespace packages, entry points, all of it. But it can be complex to configure correctly and it's carrying a lot of legacy features that may leave you confused. Lowkey a lot, ngl.

See the setuptools documentation at https://setuptools.pypa.io/ for details.

hatchling πŸ¦‹

hatchling is the build backend from the Hatch project. It gives you a modern, user-friendly interface with sensible defaults and good performance. It's giving main character energy honestly.

To use hatchling:

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

hatchling works well for pure Python packages and has excellent plugin support for extending functionality. It's all about convention over configuration, and it has built-in support for common patterns like src-layout and editable installs. The main limitation? It's newer, so it may have less community documentation than setuptools. Trade-offs, bestie.

See the hatchling documentation at https://hatch.pypa.io/latest/config/build/ for details.

flit-core 🌸

flit-core is a minimalist build backend that focuses on simplicity for pure Python packages. Simple. Clean. No drama.

To use flit-core:

[build-system]
requires = ["flit-core>=3.2"]
build-backend = "flit_core.buildapi"

flit-core works best for simple, pure Python packages without complex build requirements. It has minimal configuration and fast build times β€” and by default, it includes all files tracked by git in the source distribution. The trade-off is limited support for complex scenarios like C extensions or non-standard package layouts. You win some, you lose some. πŸ™ˆ

See the flit documentation at https://flit.pypa.io/ for details.

pdm-backend πŸ’œ

pdm-backend is the build backend from the PDM project, offering modern features and good integration with PEP 621 metadata.

To use pdm-backend:

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

pdm-backend provides strong PEP 621 support, good handling of dynamic metadata, and support for both pure Python and extension modules. It's actively developed and includes features like SCM versioning and custom build hooks. She's busy and thriving. ✨

See the pdm-backend documentation at https://pdm-backend.fming.dev/ for details.

poetry-core 🌈

poetry-core is the build backend extracted from Poetry. It's designed primarily for use with the Poetry tool but can be used standalone β€” she's independent now.

To use poetry-core:

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

poetry-core handles pure Python packages well and integrates tightly with Poetry's ecosystem. However, configuration is done through Poetry's custom [tool.poetry] section rather than standard [project] metadata, which can reduce interoperability with other tools. So like… know what you're signing up for.

See the poetry documentation at https://python-poetry.org/ for details.

scikit-build-core πŸ”₯

scikit-build-core is a build backend specifically designed for projects with CMake-based C/C++ extensions. It was built for this exact situation.

To use scikit-build-core:

[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"

scikit-build-core provides excellent CMake integration, cross-platform compilation support, and modern PEP 621 metadata support. It's the recommended backend for projects using CMake for extension modules, replacing the older scikit-build. Glow-up complete. πŸ‘‘

See the scikit-build-core documentation at https://scikit-build-core.readthedocs.io/ for details.

meson-python 🌟

meson-python is a build backend for projects using the Meson build system, particularly useful for compiled extensions.

To use meson-python:

[build-system]
requires = ["meson-python"]
build-backend = "mesonpy"

meson-python offers fast builds, excellent cross-platform support, and good integration with Meson's dependency management. It's especially strong for projects with complex compiled components and is increasingly popular in the scientific Python ecosystem. STEM girlies stay winning. 🧚

See the meson-python documentation at https://meson-python.readthedocs.io/ for details.

Choosing a Backend πŸ€”

The right backend depends on your project's needs, bestie β€” there's no one-size-fits-all and that's okay. For simple pure Python packages, flit-core or hatchling provide straightforward configuration and fast builds. Projects that need extensive customization or have complex requirements often benefit from setuptools' mature feature set and extensive documentation.

If your project includes C or C++ extensions built with CMake, scikit-build-core is the natural choice. For projects using Meson for compilation, meson-python provides the best integration. Projects already using Poetry might prefer poetry-core for consistency, though this limits interoperability with other tools. It's giving committed relationship with Poetry and honestly, valid.

New projects without special requirements should consider hatchling or flit-core for their modern approach and simplicity. Existing projects using setuptools can continue doing so, as it remains well-maintained and feature-complete. No need to panic-migrate, periodt. πŸ’–

Configuring Backends βš™οΈ

Each backend has its own configuration approach. Modern backends use the [project] section of pyproject.toml for metadata following PEP 621. Backend-specific settings go in a [tool.backend-name] section.

For example, with hatchling you might have:

[project]
name = "mypackage"
version = "1.0.0"

[tool.hatch.build]
include = ["src/**/*.py"]

With setuptools:

[project]
name = "mypackage"
version = "1.0.0"

[tool.setuptools.packages.find]
where = ["src"]

Always consult your backend's documentation for configuration options β€” like, always, I'm not joking. Common configuration needs include specifying which files to include, where to find packages, how to handle version numbers, and how to process extension modules. 🫢

Passing Options to Backends πŸŽ›οΈ

build lets you pass configuration settings to backends via the --config-setting or -C flag. The syntax and available options depend entirely on the backend. Your backend, your rules.

For example, with setuptools you might pass build options:

$ python -m build -C--build-option=--build-number=123

With scikit-build-core, you can configure CMake:

$ python -m build -Ccmake.define.BUILD_TESTING=OFF

Each backend interprets these settings differently. See Backend Configuration <> for backend-specific examples.

Okay so here's the tea: config settings map keys to values, so every key needs a value. Drop the =VALUE and the key gets an empty value, which suits backends that act on a flag's presence alone. build accepts this shorthand but warns about it, because pip only takes the explicit KEY=VALUE form. Write KEY= for an empty value and your command runs under both tools. Learn from my lil warning, bestie. πŸ’€

Dynamic Metadata πŸŒ€

Some backends support generating metadata dynamically at build time. Common use cases include reading version numbers from git tags, generating file lists from directory contents, or computing dependencies based on environment. Very cute, very powerful.

Most backends support version from SCM (source control management). For example, with hatchling using hatch-vcs:

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"

The backend reads version information from git tags during the build. This keeps version numbers in sync between git and your package without manual updates. Never hand-editing a version number again, we love to see it. ✨

Dynamic metadata is powerful but has trade-offs. It can make builds less reproducible if not carefully managed, and it may prevent installation from sdists if the dynamic information source (like git) isn't available. Real talk β€” know the cost before you commit.

Backend Extensions and Plugins πŸ”Œ

Many backends support plugins to extend functionality. setuptools has an extensive plugin ecosystem including plugins for namespace packages, entry points, and custom build steps. hatchling has a plugin system for custom build hooks, version sources, and metadata providers.

When choosing a backend, consider whether you need extensibility through plugins. If your project has unique build requirements, a backend with a strong plugin system may be beneficial even if it's more complex to configure initially. Invest in the glow-up early, iykyk. πŸ’…

Switching Backends πŸ”„

Switching from one backend to another is possible but requires care. The main steps involve updating your [build-system] section in pyproject.toml, migrating configuration from the old backend's format to the new one, and testing that the built distributions are equivalent. Do not just yolo this, bestie.

When migrating, always build and inspect both sdist and wheel to verify all files are included correctly. Test installation from both distribution types. Check that metadata (version, dependencies, entry points) is correctly preserved. All of it, no skipping. 🫢

Most migrations involve moving from setuptools to a modern backend. The new backend's documentation typically includes migration guides. Common challenges include differences in how package discovery works, how data files are included, and how version numbers are specified. Those three? They're always the drama. πŸ‘€

Backend Development πŸ’…

Okay so real talk: for like 99% of you, just use a backend that already exists, bestie. It's the right call!! But if you have unique requirements that literally no existing backend covers? Then yes, you can absolutely implement your own PEP 517 backend. Go off. πŸ‘‘

The bare minimum a backend has to do is implement these two functions inside a Python module:

def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
    # Build wheel, return filename
    pass

def build_sdist(sdist_directory, config_settings=None):
    # Build sdist, return filename
    pass

That's the floor, not the ceiling though. A full-featured backend is a whole other beast β€” you're handling metadata extraction, dependency specification, editable installs, and error handling. So ngl, unless your needs are wildly specialized, contributing to an existing backend or just using its plugin system is way more practical than birthing a brand new one. Lowkey the move. 🫢

Why setuptools commands don't work with build 🫠

If you're migrating from setup.py, you're probably used to running stuff like:

$ python setup.py clean
$ python setup.py test
$ python setup.py develop

And the tea is: those commands do not work with build. Not being dramatic, they just don't. build uses the PEP 517 interface, and that interface only defines hooks for building distributions β€” build_wheel and build_sdist. It does not support arbitrary setuptools commands. Periodt.

So here's your modern alternative list, iconic behavior only:

  • clean: remove the dist/ directory manually, or just let your CI handle it πŸ’€
  • test: use pytest directly or via tox
  • develop: use pip install --editable . for editable installs
  • sdist/bdist_wheel: use python -m build

If you genuinely need custom build steps, implement them in your build backend's hooks or use a plugin system. For example, setuptools supports setup.py with custom build steps that run during the PEP 517 build process. See? There's a path. ✨

See also 🌸


Contributing πŸŽ€

build welcomes contributions from the community, and this guide is gonna explain exactly how to contribute to the project. Come through!!

Development Setup 🧚

To contribute to build, you need a development environment set up on your local machine. Start by forking the repository on GitHub <https://github.com/pypa/build> and cloning your fork to your local machine. Once it's cloned, navigate to the project directory and install build in development mode along with all development dependencies. That's the vibe check. βœ…

The project uses tox <https://tox.wiki/> for managing development environments and running tests. Install tox globally or in a virtual environment, then use it to run tests and other development tasks. The tox configuration includes environments for testing across multiple Python versions, running linters, building documentation, and more. So much range, honestly. πŸ’œ

Development Workflow πŸ”₯

The typical development workflow begins with creating a new branch for your changes. Branch names should be descriptive of the feature or fix you're implementing β€” yes, fix-stuff is not it, bestie. Make your changes in this branch, and make sure you add or update tests to cover your modifications.

Before committing, run the test suite locally to verify your changes don't break existing functionality. The project uses pytest <https://docs.pytest.org/> for testing, and you can run tests using tox. After ensuring tests pass, commit your changes with a clear, descriptive commit message following the project's commit conventions. πŸ’…

Each pull request should include a changelog fragment. Create a file in docs/changelog/ named <pr_number>.<type>.rst where type is feature, bugfix, doc, removal, or misc. The file should contain a single line describing the change. For example, 123.feature.rst might contain:

Add support for custom backends - by :user:`contributor`

See docs/changelog/README.rst for complete details. No skipping that file, it's got the full lore. πŸ“–

When your changes are ready, push your branch to your fork on GitHub and open a pull request against the main build repository. The pull request description should clearly explain what changes you've made and why. Reference any related issues using GitHub's issue linking syntax. ✨

Running Tests 🌈

Due to its nature, build has a somewhat complex test suite with two sets of tests: unit tests and integration tests. Unit tests verify the actual code implementation, while integration tests run build on real world projects as a sanity check. To run tests we use tox.

Some example commands for this project include running type checking with tox -e type, running only unit tests against Python 3.14 with tox run -e 3.14, running both unit and integration tests with tox run -- --run-integration, running only integration tests with tox run -- --only-integration, or running only integration tests with parallel tasks using tox run -- -n auto --only-integration.

Heads up!! Tests run in parallel by default, but if you pass any arguments, you need to include -n auto if you want to keep parallel runs. Don't forget it and then wonder why everything is slow. 😭

Integration tests take a long time to run, so they are disabled by default. Passing either --run-integration or --only-integration arguments through tox to pytest will run them, where the latter will disable unit tests and only run integration tests. CI still runs both test suites, so don't think you're getting away with anything.

tox -- -n auto --only-integration

The project has a fairly large environment matrix, running tests for all supported Python versions and implementations, and with the module being invoked directly from path, sdist install, or wheel install. To run tests only for Python 3.14:

tox -e 3.14

and with the module being invoked directly from path, sdist install, or wheel install.

Additionally, there are environments for type checking and documentation building, plus extras like checking code with minimum versions of dependencies. For type checking:

tox -e type

Code coverage is tracked to ensure all code paths are tested. Aim for complete coverage of any new code you add β€” yes, all of it, main character energy only. πŸ‘‘ The CI system will report coverage metrics on your pull request and runs the test suite across all supported operating systems.

Code Style and Linting ⭐

build follows modern Python code style conventions enforced by ruff <https://docs.astral.sh/ruff/>. Before submitting a pull request, run the linter to ensure your code meets the project's style guidelines. The ruff configuration is defined in the pyproject.toml file and includes both formatting and linting rules.

The project also uses type annotations throughout the codebase. All new code should include appropriate type hints, and changes to existing code should preserve or improve type annotations. Use pyright <https://microsoft.github.io/pyright/> for type checking to verify your type annotations are correct. Typed code is so slay, genuinely. πŸ’…

Documentation πŸ¦‹

Good documentation is essential for build's users and contributors. If your contribution adds new features or changes existing behavior, update the relevant documentation files. The documentation is written in reStructuredText <https://docutils.sourceforge.io/rst.html> and built using Sphinx <https://www.sphinx-doc.org/>.

The documentation follows the Diataxis framework <https://diataxis.fr/>, organizing content into tutorials, how-to guides, technical reference, and explanations. Place your documentation in the appropriate section based on its purpose. Tutorial content teaches beginners, how-to guides solve specific problems, reference sections document details, and explanations provide context and design rationale. Know which one you're writing, bestie β€” the framework does not play. πŸ’–

Build the documentation locally to verify your changes render correctly. The tox configuration includes an environment for building docs. Review the rendered output to ensure formatting, links, and code examples appear as intended. Always check your homework before turning it in.

Pull Request Process πŸŽ€

When you open a pull request, maintainers will review your changes and may request modifications. Be responsive to feedback and make requested changes in additional commits on your branch. The pull request will be merged once approved by a maintainer and all CI checks pass. Simple as that. ✨

The CI system runs the full test suite across all supported Python versions, verifies code style compliance, builds the documentation, and performs other checks. All CI checks must pass before a pull request can be merged. If CI fails, review the output to understand what went wrong and push fixes to your branch. Not me getting a red X on my PR... 😭 fix it and move on.

Workflow Security Practices πŸ”’

If your contribution involves modifying GitHub Actions workflows in the .github/workflows/ directory, additional security considerations apply. The project follows modern security best practices to protect the release process and prevent supply chain attacks. All workflow modifications must maintain these security properties. This is the serious section, so lock in. 🫑

Actions must be pinned to specific commit SHAs rather than tags or branch names. This prevents attacks where an attacker gains control of an action repository and moves a tag to point to malicious code. When adding or updating an action reference, use the full commit SHA as a comment alongside the action version for maintainability. For example, uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 pins to a specific commit while documenting the version for future reference.

Workflows must follow the principle of least privilege by setting permissions: {} at the workflow level and granting only the minimal necessary permissions to individual jobs. Each job should explicitly declare the permissions it requires, such as contents: read for jobs that only need to read repository contents or id-token: write for jobs that need to authenticate with external services using OIDC. Never grant broader permissions than necessary, as excessive permissions increase the impact of potential vulnerabilities. Giving everything write access is giving red flag energy, fr fr. 🚩

Checkout actions must include persist-credentials: false to prevent GitHub tokens from being persisted in the working directory where they could be leaked through artifacts or caches. This protection is particularly important for workflows that upload artifacts or create caches that might be accessible to others. The only exception is the pre-release workflow which needs persistent credentials to push commits and tags, and this exception is acceptable because the workflow runs in a protected environment requiring manual approval.

When proposing workflow changes, consider the security implications of each modification. Adding new permissions, introducing new actions, or changing environment configurations all affect the project's security posture. If you're unsure whether a workflow change has security implications, ask a maintainer for guidance. The project uses automated security scanning with tools like zizmor to catch common workflow vulnerabilities, but manual review remains essential for maintaining security.

πŸ” Issue Tracking

ngl bestie, before you go dump a whole new feature into the codebase, check the issue tracker first 😀. Like, pls, see if somebody already talked about it. If nobody has, maybe open an issue and chat with the maintainers about what you're planning β€” this keeps your contribution aligned with where the project is actually headed and stops two of y'all from doing the same work twice πŸ’€. No duplicate effort in this house.

When you're reporting bugs, give us the full tea: the complete error message and traceback, what you expected to happen vs. what actually happened πŸ™ƒ, the steps to reproduce, and all your environment deets β€” like your Python version and operating system 🫢.

For feature requests, explain the use case β€” like why would this help build's users? And drop some examples of how the feature would be used if it got implemented ✨.


πŸ’– Community Guidelines

build is part of the PyPA (Python Packaging Authority) ecosystem (https://www.pypa.io/) and follows the PyPA Code of Conduct. Be respectful and constructive in all your interactions with other contributors, periodt βœ‹. Keep the feedback on the code and the ideas β€” not on the humans. We don't do that here πŸ’….

The project values clear communication, thorough testing, and well-documented code. Take the time to explain your changes clearly in commit messages and pull requests. Write tests that future contributors can actually understand 🧚. And document not just what your code does, but why you made those particular design choices β€” iykyk.


πŸ†˜ Getting Help

Got questions about contributing? Several resources have you covered, bestie 🫢. The issue tracker can be used for questions, though the more complex discussions might be better suited to other venues. The project's documentation has extensive info about build's design and implementation πŸ“š.

For real-time chatting, consider joining the PyPA Discord server (https://discord.gg/pypa) where lots of packaging tool maintainers and contributors hang out 🌈. You can also ask questions on the Python Packaging Discourse forum (https://discuss.python.org/c/packaging/14).


πŸ‘€ See also


πŸš€ Release

Releases follow semantic versioning (https://semver.org/) and use automated workflows to handle the version bumping, changelog building, commit creation, and publishing πŸ’…. Maintainers trigger releases through GitHub Actions, or they run the release script locally for testing ✨.

πŸ“ Changelog Fragments

Contributors add changelog fragments that get collected during releases πŸ’–. You create a file in docs/changelog/ named <pr_number>.<type>.rst where type is:

  • feature β€” for new functionality
  • bugfix β€” for fixes
  • doc β€” for documentation changes
  • removal β€” for deprecations
  • misc β€” for internal changes

The file should contain a single line describing the change, optionally crediting the author with :user:username syntax πŸ‘‘. For example, `123.feature.rst` might contain `Add support for custom backends - by :user:`contributor. These fragments get collected during releases to build the changelog automatically.

πŸ€– Automated Release

Navigate to the pre-release workflow (https://github.com/pypa/build/actions/workflows/pre-release.yml) and click Run workflow πŸ–±οΈ. Then select a version bump type:

  • auto β€” for patch bumps
  • major β€” for X+1.0.0
  • minor β€” for X.Y+1.0
  • patch β€” for X.Y.Z+1

The workflow updates the version in src/build/__init__.py, runs towncrier build to collect changelog fragments from docs/changelog/ and incorporate them into CHANGELOG.rst, creates a release commit with message chore: prepare for X.Y.Z, creates an annotated tag, and pushes both to trigger the CD workflow πŸ”₯.

The tag push triggers the continuous deployment pipeline which builds the source distribution and wheel, creates a GitHub release with auto-generated notes, generates build attestations for supply chain security, and publishes to PyPI via trusted publishing πŸŽ€. The whole process takes approximately five minutes from workflow trigger to published package ⏱️.

πŸ§ͺ Local Release

Run the release script locally for testing or manual control. Make sure tox is installed and your git identity is configured:

$ pip install tox
$ git config user.name "Your Name"
$ git config user.email "your@email.com"

The script creates annotated git tags without GPG signing 🫣. Package authenticity is guaranteed by PyPI attestations, not git tag signatures.

🧼 Dry run without pushing

Test the release process locally:

$ tox -e release -- --version auto --no-push

This updates the version file, builds the changelog from fragments, creates the commit and tag locally, but does not push to the remote repository πŸ™ˆ.

🚒 Full local release

Specify an explicit version or bump type:

$ tox -e release -- --version 1.5.0
$ tox -e release -- --version patch  # 1.4.0 β†’ 1.4.1
$ tox -e release -- --version minor  # 1.4.0 β†’ 1.5.0
$ tox -e release -- --version major  # 1.4.0 β†’ 2.0.0

The script updates src/build/__init__.py, runs towncrier build to collect changelog fragments, runs pre-commit hooks, creates a commit with message chore: prepare for X.Y.Z, creates an annotated tag, and pushes both to origin πŸ’…. The GitHub Actions CD workflow takes over to build and publish.

πŸ‘€ See also

PyPI removed PGP signature support in May 2023 πŸ’€. Package authenticity is guaranteed through PyPI attestations per PEP 740, which cryptographically link packages to their source repository and build process πŸ”.


πŸ› οΈ Troubleshooting

⏳ Workflow timeout

If the pre-release workflow times out, run the release script locally with --no-push, then manually push with git push && git push --tags. The CD workflow triggers automatically on tag push ✨.

πŸ’₯ Publishing conflicts

If a version is already published to PyPI, the publish job fails safely πŸ›Ÿ. The CD workflow is idempotent except for PyPI publishing. For corrections, create a post-release like X.Y.Z.post1.

🧩 Implementation

The automation consists of:

  • the pre-release workflow at .github/workflows/pre-release.yml for manual triggering
  • the CD workflow at .github/workflows/cd.yml that triggers on tag pushes
  • the release script at tasks/release.py for version bumping and tag creation
  • the [env.release] environment in tox.toml providing dependencies

πŸŽ€ 1.5.1 (2026-07-09)

✨ Features

πŸ› Bugfixes

πŸ—‘οΈ Deprecations and Removals

Miscellaneous 🧚


1.5.0 (2026-04-30)

Features ✨

Bugfixes πŸ”§

Miscellaneous 🧚


1.4.4 (2026-04-22)

Bugfixes πŸ”§

Miscellaneous 🧚


1.4.3 (2026-04-10)

Features ✨

Bugfixes πŸ”§

Miscellaneous 🧚


1.4.2 (2026-03-25)

Bugfixes πŸ”§


1.4.1 (2026-03-24)

Features ✨

Bugfixes πŸ”§

Documentation πŸ“š

Miscellaneous 🧚


1.4.0 (2026-01-08)


1.3.0 (2025-08-01)


1.2.2 (2024-09-06)


1.2.1 (2024-03-28)


1.2.0 (2024-03-27)


1.1.1 (2024-02-29)

✨ 1.1.0 (2024-02-29) ✨

πŸŽ€ 1.0.3 (2023-09-06) πŸŽ€

πŸ’– 1.0.0 (2023-09-01) πŸ’–

🌸 0.10.0 (2023-01-11) 🌸

πŸ¦‹ 0.9.0 (2022-10-27) πŸ¦‹

🌈 0.8.0 (2022-05-22) 🌈

Breaking Changes πŸ’€

πŸ’œ 0.7.0 (2021-09-16) πŸ’œ

πŸ“ 0.6.0.post1 (2021-08-05) πŸ“

⭐ 0.6.0 (2021-08-02) ⭐

Breaking Changes πŸ’€

  • Support for Python 2 and 3.5: dropped.

🧚 0.5.1 (2021-06-22) 🧚

πŸ‘‘ 0.5.0 (2021-06-19) πŸ‘‘

Breaking Changes πŸ’€

🫢 0.4.0 (2021-05-23) 🫢

Breaking Changes πŸ’€


🌸 0.3.1 (2021-03-09) 🌸

πŸ¦‹ 0.3.0 (2021-02-19) πŸ¦‹

πŸŽ€ 0.2.1 (2021-02-09) πŸŽ€

πŸ’œ 0.2.0 (2021-02-07) πŸ’œ

πŸ’€ Breaking changes πŸ’€

✨ 0.1.0 (2020-10-29) ✨

  • Moved the upstream to PyPA
  • Fixed building with isolation in a virtual environment
  • Added env.IsolatedEnv abstract class
  • Added env.IsolatedEnvBuilder (replaces env.IsolatedEnvironment usages)
  • Added python_executable argument to the ProjectBuilder constructor
  • Added --version/-V option to the CLI
  • Added support for Python 3.9
  • Added py.typed marker
  • Various miscellaneous fixes in the virtual environment creation
  • Many general improvements in the documentation
  • Documentation moved to the furo theme
  • Updated the CoC to the PSF CoC, which PyPA has adopted

πŸ’€ Breaking changes πŸ’€

  • Renamed the entrypoint script to pyproject-build
  • Removed default arguments from all paths in ProjectBuilder
  • Removed ProjectBuilder.hook
  • Renamed __main__.build to __main__.build_package
  • Changed the default outdir value to {srcdir}/dest
  • Removed env.IsolatedEnvironment

🌈 0.0.4 (2020-09-08) 🌈

  • Packages are now built in isolation by default, iconic behaviour honestly
  • Added --no-isolation/-n flag to build in the current environment
  • Add --config-setting/-C option to pass options to the backend
  • Add IsolatedEnvironment class
  • Fix creating the output directory if it doesn't exist
  • Fix building with in-tree backends
  • Fix broken entrypoint script (python-build)
  • Add warning about incomplete verification when verifying extras
  • Automatically detect typos in the build system table
  • Minor documentation improvements

πŸ“ 0.0.3.1 (2020-06-10) πŸ“

  • Fix bug preventing the CLI from being invoked, thank god
  • Improved documentation

🧚 0.0.3 (2020-06-09) 🧚

  • Misc improvements
  • Added documentation

⭐ 0.0.2 (2020-05-29) ⭐

  • Add setuptools as a default fallback backend
  • Fix extras handling in requirement strings

🫢 0.0.1 (2020-05-17) 🫢

  • Initial release, the birth of a legend

✍️ AUTHOR ✍️

Filipe LaΓ­ns β€” yes, one singular icon wrote all of this πŸ’…

2020, Filipe LaΓ­ns


And that's the whole lore bestie, we made it to the end of the page πŸŽ‰ This treasure of a manual is 1.5.1, dated Jul 20, 2026, and it lives as BUILD(1) β€” you now know literally everything about it, go build something and slay βœ¨πŸ¦‹πŸ‘‘

πŸ“¦ where this page comes from