A source distribution contains the files that a build tool uses to make a wheel. Its contents, metadata, and build steps determine whether that build works.
The format rules come from the Python Packaging Authority (PyPA). File examples come from the published packaging-25.0.tar.gz archive. [1] [2]
The hands-on examples use Python 3.12 or later and macOS/Linux shell paths. Start in a new working directory and keep using it. Here, python means that Python interpreter.
A source tree is a set of project files, such as a Git checkout. A source distribution, or sdist, is an archive of source files for a specific release. A wheel is the built distribution that an installer uses. [3]
A build tool can turn an sdist into a wheel. pip can then install that wheel. The build may compile extension modules or package pure Python code. An sdist does not mean that the project needs a compiler. [4]
Source distributions also support downstream packagers, such as Linux distributions. These packagers may apply patches, run tests, or build against their own system libraries. An sdist lets them work from a named release without a live checkout. [3] [5]
Scroll sideways to read the artifact and environment labels. Selecting a stage brings it into view.
| Item | Contents | Use |
|---|---|---|
| Source tree | Project files and build configuration | The build backend reads these files. |
| Sdist | Release source files, pyproject.toml, and PKG-INFO | The backend builds a wheel from the unpacked archive. |
| Wheel | Installable files and a .dist-info directory | The installer places files in the target environment. |
The current filename format is {name}-{version}.tar.gz. The braces name fields in the specification; they are not literal filename characters. The name and version must match the archive metadata. [1]
packaging-25.0.tar.gz
packaging, version 25.0, gzip-compressed tar archive. [2]Producers normalize the name with the wheel filename rules. They convert letters to lowercase and replace each run of hyphens, underscores, or dots with one underscore. They also use the canonical version form. The filename has no Python, application binary interface (ABI), or platform tags. [1] [6]
The archive must use the POSIX.1-2001 pax tar format, which supports UTF-8 filenames. It must contain one top-level directory named {name}-{version}. Python's tarfile module must be able to read it with mode r:gz. [1]
The filename identifies a release; it does not prove that a machine can build it. Python requirements, build dependencies, and any native toolchain still apply. A wheel records compatibility tags after the build. [1] [4] [6]
Save this as fetch_sdist.py and run python fetch_sdist.py. It fetches this release directly and checks its recorded hash.
import hashlib
from pathlib import Path
import urllib.request
url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz"
expected = "d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
with urllib.request.urlopen(url) as response:
data = response.read()
if hashlib.sha256(data).hexdigest() != expected:
raise ValueError("Archive hash does not match")
Path("artifacts").mkdir(exist_ok=True)
Path("artifacts/packaging-25.0.tar.gz").write_bytes(data)
print("Saved the verified sdist; no build backend ran.")
The archive is now artifacts/packaging-25.0.tar.gz. Nothing has been extracted or built. Use it in the examples below. [2]
The standard requires pyproject.toml and PKG-INFO at the root inside the top-level directory. PKG-INFO must use Core Metadata version 2.2 or later. For metadata version 2.4 or later, the archive must also contain each file that a License-File field names. [1]
The standard does not define the other files. The backend includes the files it needs to build the project. There is no required src/ directory, test directory, or MANIFEST.in. These depend on the project and its build backend. [1]
packaging-25.0/PKG-INFO
packaging-25.0/pyproject.toml
packaging-25.0/src/packaging/__init__.py
packaging-25.0/tests/test_tags.py
packaging-25.0/docs/index.rst
packaging-25.0/LICENSE
packaging-25.0/LICENSE.APACHE
packaging-25.0/LICENSE.BSD
packaging includes tests and documentation in this sdist. The standard does not require those files. PyPA's downstream guide recommends including the project files needed for tests and documentation as well as the build. That guide states recommendations, not format requirements. [2] [5]
An sdist has no required wheel-style RECORD file with a hash for each member. Indexes can publish a hash of the whole archive. These are different checks. [1] [6] [7]
Checkpoint 1 · source, filename, and contents
Core Metadata describes a distribution. Its fields include the name, version, Python requirement, and dependencies. PKG-INFO stores this metadata in an sdist. A wheel stores Core Metadata in .dist-info/METADATA. [8] [6]
Metadata-Version: 2.4
Name: packaging
Version: 25.0
Summary: Core utilities for Python packages
Requires-Python: >=3.8
License-File: LICENSE
License-File: LICENSE.APACHE
License-File: LICENSE.BSD
PKG-INFO headers from packaging-25.0.tar.gz. The file has no Dynamic or Requires-Dist headers. [2]The Dynamic field lists metadata fields that can change when a backend builds a wheel. It first appeared in Core Metadata 2.2. Name, Version, and Metadata-Version cannot be dynamic. [8]
Before metadata 2.6, consumers ignored an sdist field's value if the sdist marked it dynamic. For metadata older than 2.2, consumers treat all fields as dynamic. Those older files do not offer the sdist-to-wheel consistency rules above. [8]
These rules concern a wheel built from that sdist. A downloaded wheel might come from a different build path or a tool that changed an existing wheel. Do not assume that every wheel for a release has the same metadata as its sdist. [8]
The [project].dynamic key in pyproject.toml tells the backend which project fields it must supply. The Dynamic header in PKG-INFO governs changes during a later wheel build. The two declarations apply at different stages. [9] [8]
In this real example, packaging declares dynamic = ["version"] in pyproject.toml. Its sdist already has Version: 25.0 and no Dynamic headers. Core Metadata forbids a dynamic version. A backend must resolve the version before it writes the sdist metadata. [2] [8]
Save this as read_sdist.py and run python read_sdist.py. Both inputs come from the archive you downloaded.
from email.parser import Parser
import tarfile
import tomllib
with tarfile.open("artifacts/packaging-25.0.tar.gz", "r:gz") as archive:
project = tomllib.loads(archive.extractfile("packaging-25.0/pyproject.toml").read().decode())
metadata = Parser().parsestr(archive.extractfile("packaging-25.0/PKG-INFO").read().decode())
print("Project dynamic:", project["project"]["dynamic"])
print("Sdist version:", metadata["Version"])
print("Sdist Dynamic:", metadata.get_all("Dynamic", []))
Output:
Project dynamic: ['version']
Sdist version: 25.0
Sdist Dynamic: []
The project asked Flit to supply the version. By the time Flit wrote this sdist, it had resolved that value to 25.0. The version is no longer an open question for a wheel built from this sdist.
The Simple Repository API can expose Core Metadata in a separate file. Its JSON core-metadata key tells a client whether that file exists. The file URL appends .metadata to the distribution URL. A client must check the index response rather than assume that every archive has this file. [7]
A separate metadata file avoids a full archive download when the index provides it. It does not let a client treat dynamic sdist fields as final wheel metadata. The Core Metadata rules still apply. [7] [8]
A build frontend is the tool that requests a build, such as build or pip. A build backend is the project-selected code that produces the archive. PEP 517 defines the functions, called hooks, that connect them. [3]
[build-system]
requires = ["flit_core >=3.3"]
build-backend = "flit_core.buildapi"
[build-system] table in the published packaging 25.0 sdist. [2]requires lists Python dependencies needed to run the build system. build-backend names the backend that the frontend imports. These build dependencies differ from [project].dependencies, which become the package's Requires-Dist metadata. [9] [3]
For this archive, flit_core >=3.3 is a build requirement. The project declares dependencies = []. The absence of runtime dependencies does not remove its need for a build backend. [2]
| Hook | Purpose |
|---|---|
build_sdist | Create the sdist and return its filename, without its directory path. |
get_requires_for_build_sdist | Optionally report extra Python dependencies for the sdist build. |
build_wheel | Create a wheel from a source directory, including an unpacked sdist. |
get_requires_for_build_wheel | Optionally report extra Python dependencies for the wheel build. |
prepare_metadata_for_build_wheel | Optionally create wheel metadata without a full wheel build. |
prepare_metadata_for_build_sdist hook. A backend can report that it cannot build an sdist through the specified UnsupportedOperation exception. [3]build commandFor a source tree that you trust, run this command in its root directory. The Python environment must have the build package installed. Build commands execute backend code. [10] [3]
python -m build
This default checks the wheel build against the sdist's files. python -m build --sdist creates only the sdist. python -m build --wheel builds directly from the source tree. With both explicit flags, --sdist --wheel, the tool builds both files directly from the source tree. [10]
These are build tool defaults, not requirements of the sdist format. A successful wheel build checks one build path. It does not prove that the sdist contains all tests or works on every platform. [10] [5]
This step executes the release's build backend. Run it only if you trust the source and build tools. The virtual environment is not a security sandbox.
First create a separate Python environment and install the frontend:
python -m venv .venv
.venv/bin/python -m pip install --index-url https://pypi.org/simple build
Save this as unpack_sdist.py, then run python unpack_sdist.py. It extracts the verified archive into source/.
import tarfile
with tarfile.open("artifacts/packaging-25.0.tar.gz", "r:gz") as archive:
archive.extractall("source", filter="data")
Now ask the frontend to build from that extracted source tree:
.venv/bin/python -m build source/packaging-25.0 --outdir built
The checked run used build 1.6.1 and selected flit-core 4.0.2. It created these files:
built/packaging-25.0.tar.gz
built/packaging-25.0-py3-none-any.whl
The frontend first created a new sdist from the extracted source. It then built the wheel from that new sdist. You did not need the project's Git repository. Backend versions can change because flit_core >=3.3 is not an exact pin.
Checkpoint 2 · metadata and the build
packaging 25.0 sdist and wheel. License files also change location. [2] [19]The backend chooses which project files enter the sdist. A Git archive does not automatically meet the sdist format. It must also have the required metadata, build configuration, directory layout, and filename. [1]
For setuptools, MANIFEST.in adds or removes files from the sdist. Setuptools processes its commands in order. It also has default include rules and plugins that can use tracked files. These are setuptools features; MANIFEST.in is not a file-selection standard for all backends. [11]
A file in an sdist does not automatically enter its wheel. With setuptools' include_package_data=True, the default wheel inclusion applies to files inside package directories. Tests outside those directories can remain in the sdist without entering the wheel. [11]
packaging 25.0 uses Flit instead. Its [tool.flit.sdist] table explicitly includes LICENSE*, tests/, docs/, and CHANGELOG.rst. It also lists exclusions. The selected file paths above come from the resulting archive, not from an assumed directory layout. [2]
Keep release builds independent of an unavailable Git checkout. Version tools can use tags that a source archive does not contain. PyPA's downstream guide recommends resolving these inputs when you create the release archive. It also recommends builds that can use dependencies supplied without network access. [5]
Use the files in built/ from the previous example. Save this as compare_files.py and run python compare_files.py.
import tarfile
from zipfile import ZipFile
with tarfile.open("built/packaging-25.0.tar.gz", "r:gz") as source:
source_files = source.getnames()
with ZipFile("built/packaging-25.0-py3-none-any.whl") as wheel:
wheel_files = wheel.namelist()
print("Test module in sdist:", "packaging-25.0/tests/test_tags.py" in source_files)
print("Test module in wheel:", "tests/test_tags.py" in wheel_files)
print("Package module in wheel:", "packaging/tags.py" in wheel_files)
Output from that build:
Test module in sdist: True
Test module in wheel: False
Package module in wheel: True
The sdist keeps the test source for people who build and test the project. The wheel contains the package module, without that test file. File inclusion in one archive does not imply inclusion in the other.
<site-packages> denotes your target environment’s package directory. This does not run an installation. [6] [19]For a source build, pip creates an isolated build environment and installs the build dependencies there. It obtains metadata when needed, then asks the backend for a wheel. pip installs the wheel, rather than copying the sdist's source tree into the environment. [4]
If the backend provides prepare_metadata_for_build_wheel, pip can use that hook. Otherwise, pip builds a wheel and reads its metadata. Thus a request for metadata can run a build, even before installation. [4]
Build isolation keeps build-time Python dependencies separate from the user's runtime environment. It does not supply every native compiler, header, or system library a project might need. The backend and project determine those requirements. [4] [5]
--no-build-isolation--no-binary :all:--only-binary :all:--find-links. A requirement fails if no suitable binary distribution is available there. Explicit source URLs and editable installs can still trigger builds. This option is not a general block on build-code execution. [12] [14] [18]pip downloadpip install does, but saves distributions instead of installing them. Do not treat it as a raw file fetch: metadata work can call the build backend. [15] [4]Checkpoint 3 · selection, isolation, and binaries
Only build source code that you trust. A backend is executable code, and its hooks can run code supplied with a project. Metadata generation can also execute code. pip's build isolation separates Python dependencies; it is not a security sandbox for that code. [3] [4] [14]
Use a separate, restricted environment when you must examine an untrusted build. Do not provide release credentials to it. This is operational advice based on the code-execution risk, not a guarantee from the sdist standard.
The sdist specification requires tools to use tarfile.data_filter or meet its alternative extraction rules. These rules forbid writes outside the destination, links outside the destination, and device files, including pipes. They also require tools to clear high permission bits. [1]
Python 3.14 uses the data extraction filter by default. Older Python versions can have different defaults. Check for tarfile.data_filter support instead of relying only on a version number, because some security updates backport it. [16]
The data filter does not make every archive safe. Python's documentation warns that it does not prevent denial-of-service attacks. For untrusted files, use limits on disk, memory, and CPU use, and inspect archive members before extraction. [16]
A hash checks whether bytes match an expected file. It does not prove that the file's code is safe. A hash fetched from the same server as the archive also does not protect against that server changing both. pip distinguishes index-provided hashes from hashes pinned locally with --require-hashes. [14]
The Python script below fetches the published packaging 25.0 sdist over HTTPS. It checks the archive against the SHA-256 value recorded in PyPI's release data. It then reads PKG-INFO in memory. It does not extract files to disk, import the package, or call a build backend. [2] [16]
Run this as a Python script. It uses only the Python standard library and needs network access to files.pythonhosted.org. The URL and hash identify this exact release.
import hashlib
import io
import tarfile
import urllib.request
url = (
"https://files.pythonhosted.org/packages/a1/d4/"
"1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/"
"packaging-25.0.tar.gz"
)
expected = "d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
with urllib.request.urlopen(url, timeout=30) as response:
data = response.read()
if hashlib.sha256(data).hexdigest() != expected:
raise ValueError("Archive SHA-256 does not match")
with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive:
member = archive.getmember("packaging-25.0/PKG-INFO")
if not member.isfile():
raise ValueError("PKG-INFO is not a regular file")
with archive.extractfile(member) as metadata:
print(metadata.read().decode("utf-8"))
The output includes the metadata headers shown earlier, followed by the project's description. This script checks a specific small archive. It is not a general validator or a resource-limited scanner for unknown archives.
Check the archive that you plan to publish. A build from a checkout cannot show whether the sdist omitted a file. The default python -m build uses the sdist for the wheel build. Use that path in the release process. [10]
Also run the project's relevant tests against the built package. If downstream users need the tests or documentation sources, check that the archive contains them. A successful wheel build does not exercise those files. [5]
Twine uploads existing distribution files. Its twine check command checks whether the long description will render on PyPI. That check is not a substitute for a build or the project's tests. [17]
| Failure | Check |
|---|---|
| A file exists in the checkout but not in the sdist | Read the archive's member list. Check the backend's include and exclude rules. For setuptools, check MANIFEST.in and stale build metadata. [11] |
| The backend cannot import a build dependency | Check [build-system].requires and the extra requirements from the relevant build hook. If isolation is disabled, check the supplied environment. [4] |
| A compiler or a system header is missing | Read the project's native build requirements. Python build isolation manages Python dependencies, not the full operating-system toolchain. [4] [5] |
| The build cannot determine a version without Git | Check how the version tool handles an unpacked sdist. The archive must contain the fixed release version in PKG-INFO. [1] [5] |
| A build passes but a runtime data file is absent | Check the wheel's contents as well as the sdist's contents. Backend rules can include a file in only one of them. [11] |
Do not read build isolation as a promise of identical bytes across builds. This archive declares flit_core >=3.3, not one exact backend version. Record the actual tools and environment when you need to investigate build differences. [2] [10]
Checkpoint 4 · risk, inspection, and release
The current format uses pyproject.toml to identify a modern sdist. Older source archives follow rules set by tools such as distutils. The current format specification does not standardize those older layouts. [1]
A legacy sdist that contains PKG-INFO version 2.2 or later must still follow the relevant Core Metadata rules. A legacy archive is not exempt from those rules merely because it lacks the current layout. [1]
setup.py is not a required file in a modern sdist. A backend may need it, and older projects can still contain it. pip documents a setuptools fallback for a project with setup.py but no [build-system] section. [3] [4]
Dynamic field. The living Core Metadata specification includes later changes. Read PEP 643.Use the living PyPA specifications for current requirements. Read the PEPs for the decisions that led to them. [1] [8]
Format claims cite PyPA specifications. Tool behavior cites the tool's documentation. Archive examples cite the published release. Sources were checked on 13 September 2026; tool documentation can change. The workshop’s sdist and wheel excerpts were rechecked against their pinned SHA-256 hashes on 11 October 2026.
Dynamic rules.--no-binary and locally built wheel caching.29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484; inspected without importing package code.