build
build is the PyPA's build frontend: the tool you run to turn a source tree with a pyproject.toml into an sdist and a wheel in dist/. It does not know how to compile anything itself. It reads the [build-system] table, creates an isolated virtual environment, installs the requires listed there, then calls the PEP 517 hooks on whichever backend you named (setuptools, hatchling, flit-core, poetry-core, maturin, scikit-build-core). That indirection is the whole point: python -m build produces the same artifacts regardless of which backend the project chose, which is why it replaced the old python setup.py sdist bdist_wheel invocation.
The correct default way to produce Python distributions when you are not already inside a project manager that builds for you. Boring, tiny, and maintained by the people who write the packaging specs, which is exactly what you want from this part of a release pipeline.
Use it if
- You publish packages and want one command that works the same across setuptools, hatchling, flit-core, poetry-core, and maturin projects
- You need reproducible CI artifacts: the default isolated build means your local site-packages cannot leak into the wheel, which is the classic cause of 'works on my machine' releases
- You want to read a project's wheel metadata (name, version, requires-dist) without installing it, either via --metadata on the CLI or build.util.project_wheel_metadata in Python
- You are still running python setup.py sdist bdist_wheel: that path is deprecated and this is the direct replacement
- You already use uv, hatch, poetry, or flit as your project manager: each has its own build command that does the same job without a second tool in the chain
- You expected it to publish too: build only writes files into dist/, so you still need twine or uv publish, and it will not bump versions, tag, or sign anything
- Build speed matters in a tight loop: isolation creates a fresh venv and reinstalls every build requirement on each invocation, so an editable-install workflow or --no-isolation is far quicker for iterating
- You are packaging inside an air-gapped or offline environment: isolation wants to reach an index to fetch build requirements, and turning it off with -n means you own installing them correctly yourself
- You want a build backend: this is only a frontend, so it is useless without setuptools or hatchling or similar declared in pyproject.toml, and a project with no [build-system] table falls back to setuptools whether you wanted that or not
Setup reality
pip install build is trivial (packaging, pyproject_hooks, plus tomli on Python below 3.11) and needs Python 3.10 or newer as of 1.5.0. Most people should not install it into the project environment at all: pipx run build or uvx --from build pyproject-build keeps it out of your dependency tree. The friction shows up in isolation. Every run creates a temporary venv and downloads build requirements, so cold builds are slow and offline builds fail. Passing -n disables isolation but then the backend must already be importable in the current interpreter, and -x additionally skips the dependency check that would have told you what was missing. Backend options go through -Csetting=value, which setuptools supports only partially, so a flag that works with hatchling may be quietly ignored elsewhere.
Patterns
Build both distributionsbuild-sdist-and-wheel
$ python -m build
# dist/mypkg-1.2.0.tar.gz
# dist/mypkg-1.2.0-py3-none-any.whlThe default builds the sdist first and then builds the wheel from the unpacked sdist, which catches missing files in MANIFEST.in before your users do.
Build just one artifactbuild-wheel-only
$ python -m build --wheel # or -w
$ python -m build --sdist # or -s
$ python -m build --sdist --wheel # -sw: both, wheel built from the source tree-sw is not the same as the default. It builds both from the working directory, so a file missing from the sdist will not show up as a broken wheel.
Write artifacts somewhere other than dist/choose-output-directory
$ python -m build --outdir build-artifacts/
$ python -m build -o "$RUNNER_TEMP/wheels" .The final positional argument is the source directory and defaults to the current directory, so ordering matters when you pass both -o and a path.
Run build without adding it to your projectrun-without-installing
$ pipx run build
$ uvx --from build pyproject-build --installer uv
$ pipx run 'build[uv]' --installer uvKeeping the frontend out of the project environment avoids version conflicts with your own dependencies. pyproject-build is the console script name; python -m build is the same code.
Use uv to create the isolated environmentspeed-up-with-uv
$ python -m build --installer uv
# in pyproject.toml for cibuildwheel:
# [tool.cibuildwheel]
# build-frontend = "build[uv]"--installer only changes how the isolated env is created and populated. It does not change the backend or the resulting artifacts, and uv must already be on PATH unless you installed the [uv] extra.
Build against the current environmentdisable-isolation
$ python -m build --no-isolation # -n
$ python -m build -nx # also skip the dependency check
# equivalent to pip's and uv's --no-build-isolationWith -n the backend and every build requirement must already be installed in the active interpreter, or the run fails with an import error from the hook. -x hides the check that would have named the missing package.
Pass config settings to the backendpass-backend-options
$ python -m build -Cbuild-arg=--verbose -Cbuild-arg=--jobs=4
$ python -m build --config-json '{"builddir": "out", "setup-args": ["-Dopt=1"]}'Repeating the same -C key collects values into a list. Backends differ wildly in what they accept, setuptools support is very limited, and unknown keys are usually ignored rather than rejected.
Get project metadata without building a wheelread-metadata-only
$ python -m build --metadata | jq -r .version
$ python -m build --metadata dist/mypkg-1.2.0-py3-none-any.whlThis uses the backend's prepare_metadata_for_build_wheel hook when available and falls back to a full wheel build when not, so it is not always fast. --metadata cannot be combined with --sdist, --wheel, or --outdir, and the source argument can be an already-built .whl to read metadata straight from it.
Build from Python with an isolated environmentpython-api-build
from build import ProjectBuilder
from build.env import DefaultIsolatedEnv
with DefaultIsolatedEnv(installer='uv') as env:
builder = ProjectBuilder.from_isolated_env(env, '.')
env.install(builder.build_system_requires)
env.install(builder.get_requires_for_build('wheel'))
path = builder.build('wheel', 'dist/')
print(path)You have to install build_system_requires and get_requires_for_build yourself; from_isolated_env only wires the runner to the venv. Skipping either step makes the backend hook fail with a bare ImportError.
Read wheel metadata from Pythonpython-api-metadata
from build.util import project_wheel_metadata
meta = project_wheel_metadata('.', isolated=True)
print(meta['Name'], meta['Version'])
print(meta.get_all('Requires-Dist'))The return value is an importlib.metadata PackageMetadata, so repeated headers such as Requires-Dist need get_all(); plain indexing gives you only the first one.
Find out what a non-isolated build is missingcheck-build-dependencies
from build import ProjectBuilder
builder = ProjectBuilder('.')
unmet = builder.check_dependencies('wheel')
if unmet:
for chain in unmet:
print(' -> '.join(chain))Each entry is a chain showing which requirement pulled in the unmet one. This is exactly the check that -x turns off on the CLI.
Build and upload in a release workflowrelease-in-ci
- run: pipx run build
- run: pipx run twine check --strict dist/*
- uses: pypa/gh-action-pypi-publish@release/v1 # trusted publishing, no tokentwine check --strict catches a README that PyPI will reject, which build itself never validates. build stops at dist/ and deliberately has no upload command.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| uv | PyPI | You want uv build plus publish in one fast Rust binary and are already using uv to manage the project |
| hatch | PyPI | You want an opinionated project manager where building, versioning, environments, and publishing are one tool |
| twine | PyPI | The artifacts already exist in dist/ and you need to check and upload them, which build deliberately does not do |
| flit | PyPI | The project is a simple pure-Python package and you want build plus publish with almost no configuration |