How to Ship a LICENSE File in a Python Wheel
A LICENSE file in your repository does not guarantee that it reaches the wheelA prebuilt Python package file (.whl) that installs without compiling anything. The standard distribution format for Python packages.
Learn more โ
. Declare the file in pyproject.toml, build the wheel, and inspect the archive before publishing it.
This configuration also survives a build backend change. Hatchling may discover a top-level LICENSE on its own, while uv_build packages it when [project] license-files declares it.
Prerequisites
- uv installed
- A package project with a
[build-system]table inpyproject.toml - A UTF-8 license file in the project directory, such as
LICENSE
Declare the license and its file
Add both fields under the existing [project] table:
[project]
license = "MIT"
license-files = ["LICENSE"]license records the package’s SPDX license expression. license-files tells the build backend which full license text to include in distribution archives. One field does not replace the other.
Replace MIT with the expression for your project. A literal filename is a valid pattern. For several legal files, use patterns such as LICEN[CS]E* or NOTICE*; patterns can also target a subdirectory. Patterns are relative to pyproject.toml; write paths with /.
Build the wheel
Run the build from the directory containing pyproject.toml:
uv build --wheelFor a project named licensed-demo at version 0.1.0, the output ends with:
Successfully built dist/licensed_demo-0.1.0-py3-none-any.whl
uv build calls the backend declared in [build-system], so this command works whether the project uses uv_build, Hatchling, setuptools, or another PEP 517 build backend.
Inspect the built wheel
List only the license entries from the newest wheel in dist/:
uv run --no-project python -c "from pathlib import Path; import zipfile; wheel = max(Path('dist').glob('*.whl'), key=lambda path: path.stat().st_mtime); print(*[name for name in zipfile.ZipFile(wheel).namelist() if '.dist-info/licenses/' in name and not name.endswith('/')], sep='\n')"The project from the build example prints:
licensed_demo-0.1.0.dist-info/licenses/LICENSE
The .dist-info/licenses/ path proves that the file is inside the artifact users install. The wheel’s METADATA file also records it in a License-File: LICENSE field.
Fix a missing license entry
An empty inspection result means the newest wheel has no file under .dist-info/licenses/. Check that every license-files pattern matches at least one UTF-8 file relative to pyproject.toml, then rebuild the wheel.
Do not rely on a backend’s automatic file discovery. Keeping license-files in the standard [project] table makes the requirement visible and preserves it when the project changes build backends.