Python Build Tools: A Guide for Developers
Understand Python build frontends, backends, pyproject.toml, and distribution files, then choose a backend that fits your package.
Python package builds use two components: a frontend such as build to start and coordinate a build, and a backend such as Hatchling or setuptools to create the package files. For a new pure-Python library, start with pyproject.toml, use the standard [project] metadata table, choose a documented backend, then run python -m build and inspect both the wheel and source distribution before publishing.
This guide focuses on building and distributing Python packages. Application bundlers and environment managers are different categories of tools. The Python Packaging User Guide recommends [project] metadata for new projects, while legacy setup.py and setup.cfg remain supported for compatibility and special cases.
1. What Python build tools do
A build turns a project’s source tree and metadata into distribution artifacts. The two principal artifacts are:
- Wheel (
.whl): an installable distribution. Wheels may be pure Python or platform-specific when they contain compiled extensions. - Source distribution (
.tar.gz, often called an sdist): the source files needed to build or inspect the project.
The backend controls package-specific work such as file discovery, metadata generation, and artifact creation. That makes backend configuration consequential: a successful command does not prove the intended files made it into the artifacts. Review their contents before release. See PyPA’s packaging tutorial.
2. Frontend vs. backend
| Part | Role | Examples |
|---|---|---|
| Frontend | Reads project configuration, prepares the build environment, and calls standardized build hooks. | build |
| Backend | Implements the hooks and decides how project files and metadata become distributions. | Hatchling, setuptools, Flit, PDM backend, scikit-build-core, meson-python |
The separation lets a frontend work with multiple backends. In a typical PEP 517 build, the frontend reads [build-system], installs its declared build requirements into an isolated environment, and invokes backend hooks. The backend supplies metadata and builds the requested wheel or sdist. The build documentation explains this workflow.
3. Which Python build backend should I use?
| Project need | Candidate | Tradeoff to consider |
|---|---|---|
| Small, straightforward pure-Python library | Flit-core or Hatchling | Both suit simple package layouts. Hatchling has plugin support and common layout conventions; choose based on the configuration and plugins you need. |
| Existing package with extensive customization, namespace packages, entry points, C extensions, or legacy setup | setuptools | Mature and flexible, with more legacy concepts and configuration choices. |
| C or C++ extension built with CMake | scikit-build-core | Connects the Python package build to CMake. |
| Extension project already based on Meson | meson-python | Integrates packaging with Meson. |
| Workflow centered on Poetry | poetry-core / Poetry | Fits that ecosystem. Poetry 2.0 and later supports standard [project] metadata; custom [tool.poetry] metadata can reduce interoperability in some contexts. |
| PDM workflow or need for its dynamic metadata/build hooks | pdm-backend | Offers standard metadata support alongside backend-specific features. |
These are use-case distinctions, not a speed ranking. There is no performance comparison here. Confirm a backend’s current capabilities and setup in its own documentation before migrating; PyPA’s backend guide outlines the frontend/backend relationship and backend choices.
A practical decision path
- If the package is pure Python and conventional, compare Hatchling and Flit-core based on package layout and required plugins.
- If you already depend on setuptools-specific behavior, first check whether it can express your project cleanly through
[project]plus its own configuration. Keeping setuptools is a valid choice. - If compilation is central, choose around the native build system: CMake points toward scikit-build-core, Meson toward meson-python.
- If a team’s workflow depends on Poetry or PDM features, value that integration against the portability of standard metadata.
- Build and inspect artifacts before changing backends in a release branch. Compare included files, generated metadata, and installation behavior.
4. Configure a new pure-Python package
Here is a small runnable package using Hatchling and the standard metadata table. The minimum version shown is an example from the current PyPA guide, not a permanent compatibility promise; check the backend documentation when you adopt it.
pyproject.toml
[build-system]
requires = ["hatchling >= 1.26"]
build-backend = "hatchling.build"
[project]
name = "greeting-card"
version = "0.1.0"
description = "A small greeting helper"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
license-files = ["LICENSE"]
dependencies = []
[project.optional-dependencies]
dev = ["pytest"]
[project.scripts]
greeting-card = "greeting_card:main"
[project.urls]
Homepage = "https://example.com/greeting-card"
# Project files:
# README.md
# LICENSE
# src/greeting_card/__init__.py
Example module at src/greeting_card/__init__.py:
def greet(name: str = "world") -> str:
return f"Hello, {name}!"
def main() -> None:
print(greet())
The table names the backend requirement and its import path. Build requirements belong in [build-system].requires; runtime dependencies belong in [project].dependencies. Do not put a runtime library in build requirements unless the backend itself needs it to build. For a console command, the entry point must refer to an importable module and callable.
5. Common pyproject.toml settings
pyproject.toml can contain three kinds of tables. The standard roles are defined in the pyproject.toml specification and PyPA’s configuration guide.
| Table | Use it for |
|---|---|
[build-system] |
Build backend import path and build-time requirements. Include it when declaring the backend. |
[project] |
Standard package metadata: name, version, Python requirement, dependencies, README, license, and related fields. |
[tool.*] |
Tool-specific configuration, including backend-specific settings. Consult that tool’s documentation. |
Metadata fields developers commonly need
nameis required and cannot be dynamic. Project names are case-insensitive and normalize runs of hyphens, underscores, and periods.versionis normally written directly or declared dynamic for a backend-supported source such as a version attribute or Git tag.requires-pythoncommunicates the Python versions the package supports and affects install eligibility. Classifiers are useful metadata but do not replace this field.dependenciescontains required install dependencies.[project.optional-dependencies]defines extras such asdevorcli, installable aspackage-name[dev].readmepoints to the long description file, oftenREADME.md. Metadata can also include authors, maintainers, description, keywords, classifiers, project URLs, scripts, GUI scripts, and plugin entry points.licenseuses an SPDX license expression in the current format, andlicense-filesnames legal files or supported globs to include. Backend support is version-sensitive: the PyPA guide lists thresholds including Hatchling 1.27.0, setuptools 77.0.3, Flit-core 3.12, pdm-backend 2.4.0, poetry-core 2.2.0, and uv-build 0.7.19.
When metadata is dynamic, list the field in dynamic and configure the backend to supply it. Do not set the same field both statically and dynamically. A backend must know how to generate every dynamic field, so verify its documented mechanism.
6. Backend declarations you will see
Use each backend’s current installation and configuration documentation. These examples identify common import paths; version constraints can change with support for newer metadata and features.
# Hatchling
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# setuptools
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
# Flit
[build-system]
requires = ["flit_core"]
build-backend = "flit_core.buildapi"
# PDM backend
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
# uv-build
[build-system]
requires = ["uv_build"]
build-backend = "uv_build"
Only one [build-system] table and backend declaration applies to a project. Don’t copy all these blocks into one file. The PyPA guide shows example minimum versions; use those as dated examples and check backend documentation for your actual requirements.
7. Build a wheel and source distribution
From the project root, install the frontend and build:
python -m pip install --upgrade build
python -m build
By default, this creates both formats in dist/: a wheel and an sdist. To create only one:
python -m build --wheel
python -m build --sdist
These commands are alternatives; run the one or ones you need. Isolated builds install the declared build requirements in a temporary environment. If your environment is offline or tightly controlled, prepare those requirements there or deliberately use a non-isolated build with dependencies already installed; the latter makes the caller responsible for a correct build environment.
Inspect before publishing
- List files in
dist/and ensure the expected wheel and sdist exist. - Inspect archive contents. For a wheel, use
python -m zipfile -l dist/*.whl; for an sdist, usetar -tzf dist/*.tar.gz. - Check that the README, license files, package modules, and any required data files are included, and that tests or local-only files are not unintentionally shipped.
- Inspect wheel metadata by listing the
.dist-infofiles in the wheel and reviewingMETADATAand entry-point metadata. - Install the wheel into a fresh virtual environment and exercise the public import and command entry points.
- For an extension module, verify that the wheel matches the intended platform and Python compatibility and that the sdist includes enough source and build configuration for downstream builds.
8. Legacy setup.py and setup.cfg
setup.py is not categorically invalid or unusable. Setuptools still supports legacy configuration, and a Python file remains useful when genuinely programmatic setup is required. For a new project, standard metadata in [project] is the clearest interoperable starting point. Existing projects can migrate incrementally: declare a backend in pyproject.toml, move supported static metadata to [project], retain backend-specific configuration where needed, then compare artifacts.
Avoid using python setup.py install or python setup.py sdist as the normal build interface. Use a frontend such as python -m build to invoke the backend’s standardized hooks. For details on setuptools’ supported patterns, consult its user guide.
9. Extensions, data files, and package layouts
Pure-Python projects can often use conventional package discovery. Native extensions add a compiler and platform layer: users may need compatible wheels, or their installer may build from the sdist. If using CMake or Meson already, prefer a backend integrated with that system. With setuptools, extension configuration may require backend-specific setup.
Decide deliberately which non-code files are runtime package data and which are repository or test files. Backend defaults differ, and a file present in your checkout may be absent from one or both artifacts. Check wheel and sdist separately: the sdist generally needs source and build inputs, while the wheel needs the files users need after installation.
The PyPA starter layout uses a license, pyproject.toml, README, src/ package, and tests directory. A src/ layout can help distinguish installed package files from repository-root files, but use the discovery configuration supported by your chosen backend.
10. Troubleshooting Python package builds
| Symptom | Likely cause | What to check or fix |
|---|---|---|
| No backend specified or frontend cannot identify a backend | Missing or malformed [build-system]. |
Add the table with both requires and build-backend, using the backend’s documented import path. |
| Backend import or build dependency not found | Wrong requirement name, missing network access, or invalid build isolation setup. | Confirm the package name and backend path; make the declared build requirements available to the isolated environment. |
| “Backend does not support” a metadata field | Backend version is too old or the field uses a newer format. | Check backend feature/version documentation. For SPDX license expressions, use a backend version that supports the current format. |
| Field is missing from built metadata | It was marked dynamic without backend configuration, omitted from metadata, or unsupported. | Check the generated METADATA; either declare it statically or configure the backend’s documented dynamic source. |
| Module imports locally but not after installation | Package discovery or wheel inclusion missed the module. | Inspect wheel contents and configure package discovery/layout in the backend. |
| Data or license file is absent | Backend inclusion defaults or file patterns differ from expectation. | Inspect both archives; add backend-specific inclusion rules and valid license-files patterns as appropriate. |
| Console command fails after install | Entry-point target is misspelled or not importable. | Verify the module:callable path and install the wheel into a clean environment. |
| Native extension build fails | Compiler, system library, CMake/Meson configuration, or platform mismatch. | Install the required native toolchain, check backend configuration, and build/test on the target platform. Prefer matching wheels where available. |
| Build passes on one machine but fails in CI or offline | Undeclared build requirement, network-dependent build, or reliance on ambient environment. | Declare build dependencies in [build-system].requires; pin or constrain intentionally and make requirements available in the build environment. |
| Duplicate metadata or conflict between pyproject and setup configuration | The same field is defined in more than one configuration system. | Follow the backend’s precedence rules and keep each metadata field in one authoritative place. |
11. Performance, reliability, and cost
There is no supported benchmark here for claiming one backend is universally faster. Build time depends on what the package does, especially compilation, file discovery, and environment setup. For routine reliability, declare all build requirements, build from a clean checkout, and inspect both artifacts. CI should build the same distribution files you intend to publish and retain them for release review.
Build tools are generally open-source software, but builds can consume CI minutes, download time, and native compiler resources; this guide does not compare current vendor pricing. Isolated builds improve separation from ambient packages but require access to declared build requirements. Reusing a prepared environment can reduce repeated setup, provided its dependencies are controlled and match the declaration.
12. Website screenshots for packaging documentation
If a Python package’s documentation includes a live website screenshot, packaging is a separate concern from capturing the page. You can use a browser automation library yourself, or use ScreenshotNeo, a website screenshot API and MCP server from Yorker Media. It returns an image or PDF from one GET request.
Do it yourself with Python and Playwright
This minimal example launches Chromium, navigates to a page, saves a full-page PNG, and closes the browser. Install Playwright and its Chromium browser first:
python -m pip install playwright
python -m playwright install chromium
import asyncio
from pathlib import Path
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(viewport={"width": 1440, "height": 900}, device_scale_factor=1)
await page.goto("https://example.com", wait_until="networkidle", timeout=60000)
await page.screenshot(path="page.png", full_page=True)
await browser.close()
asyncio.run(main())
For dynamic pages, choose the readiness signal that matches the site: wait for a key selector when possible, or use an appropriate load state and a bounded timeout. Network idle can be unsuitable for pages with persistent connections or constant background requests. For a focused region, use a locator screenshot instead of full_page=True. Browser automation gives you control, but you manage browser installation, execution, waiting, and failure handling.
Or skip the browser setup
Use ScreenshotNeo’s screenshot endpoint with the target URL. See the ScreenshotNeo API documentation for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
- Cookie banners are accepted and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture; these steps can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for Claude, Cursor, and other MCP clients. - The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000; every feature is on every plan.
Sign up free for 1,000 screenshots a month, no card required.
13. Frequently asked questions
Do I need to install the build backend globally?
No. A frontend such as build can create an isolated environment and install the project’s declared build requirements for the build.
Can one project use different backends for different artifacts?
A project declares one backend in its build-system configuration. That backend may implement hooks to produce the supported artifacts.
Does declaring a Python version classifier enforce it?
No. Set requires-python to express the interpreter compatibility requirement; classifiers primarily describe and categorize the project.
Should I commit generated wheel files?
Usually the source repository contains the inputs, while build artifacts are produced for a release. Follow your project’s release workflow and avoid treating a stale local artifact as the source of truth.


