In the pyproject.toml project table, only name must be written out statically. version is also required, but you may list it as dynamic so your build backend fills it in. Every other field is optional, though a library you publish should set several more.
Required, required but dynamic, and optional
The pyproject.toml specification splits the project table into three groups. This is the part people usually get wrong.
| Group | Fields | Notes |
|---|---|---|
| Must be static | name | Cannot be listed as dynamic |
| Required, may be dynamic | version | Static value or filled by the backend |
| Optional | everything else | Static, dynamic, or omitted |
If your pyproject.toml has no project table at all, the spec says the build backend provides all keys dynamically. That is legal, but it makes your metadata invisible to anyone reading the file.
What dynamic means
Static metadata is written directly in pyproject.toml. Dynamic metadata is listed under the dynamic key and computed by the backend at build time.
[project]
name = "example-library"
dynamic = ["version"]
When a field is dynamic, the specification makes it the backend's responsibility to fill it, and a backend must raise an error if it cannot. A common use is reading the version from a Git tag or from a __version__ attribute in your code.
One more rule is worth knowing. A list or table field can be specified statically and also listed as dynamic. The static entries are then fixed, and the backend may only append to them.
What a published library should also set
The minimum above gets you a build. A package other people depend on should carry more, because these fields are what appear on the PyPI project page and in installers.
- requires-python. Declares the Python versions you support. Installers use it to pick a compatible release.
- dependencies. Your runtime requirements, with bounds you have tested.
- readme. Points at a README file whose extension sets how PyPI renders it.
- license and license-files. A license expression plus the files that carry the text.
- classifiers and urls. Search and browsing aids, plus links to source, issues and changelog.
- authors or maintainers. Who to contact.
For the license pair, see the PEP 639 explainer.
A realistic small example
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "example-library"
dynamic = ["version"]
description = "One line that shows on the PyPI project page."
readme = "README.md"
requires-python = ">= 3.9"
license = "MIT"
license-files = ["LICENSE"]
dependencies = ["httpx >= 0.27"]
The build backend decides where a dynamic version comes from, so check its documentation for the matching setting.
Why this matters beyond tidiness
Scanners and installers read this metadata. The name and version decide which advisories match your package, and the declared dependencies decide what your users pull in. If the version is wrong or missing from published artifacts, nothing downstream can reason about it correctly.
Safeguard's dependency scanning and SBOM generation read declared dependencies and versions from projects. A free scan shows what your own metadata exposes.
Quick check
Can name be dynamic?
No. The specification lists name as the only key required to be statically defined.
Do I have to set a version by hand?
No. Version is required, but you can mark it dynamic and let your build backend compute it from a tag or from your code.
What happens with no project table?
The backend is then expected to provide all keys dynamically. It is valid, but the metadata is hidden from readers of the file.
Sources
- pyproject.toml specification, Python Packaging User Guide
- Writing your pyproject.toml, Python Packaging User Guide
- Single-sourcing the Project Version, Python Packaging User Guide
Last reviewed: 19 October 2026.