For users

Plugins are configured as an ordered array of tables, [[tool.dynamic-metadata]]. Each entry must specify a provider exposing the plugin API; everything else in the entry is passed to that plugin as settings.

An installed plugin is referenced by the name it registers in the dynamic_metadata.provider entry-point group. Names are conventionally prefixed with the providing package, so the bundled plugins are dynamic_metadata.regex, dynamic_metadata.template, and so on:

[[tool.dynamic-metadata]]
provider = "dynamic_metadata.regex"
# ... plugin settings ...

Run dynamic-metadata providers to see the names available in your environment. A plugin that lives inside your own project rather than an installed distribution is instead given as an inline table with path and module keys.

Entries run in order, so a later entry sees every field an earlier entry produced. This makes resolution order explicit (no dependency graph), lets you modify one field with several plugins, and means a plugin can read another field’s value simply with project[...]. Plugins can, if desired, use their own tool.* sections as well.

Your build backend must support dynamic-metadata for this to work. Build backends known to support this currently include:

  • scikit-build-core (1.0+)

Example: regex

The bundled regex plugin is used like this:

[build-system]
requires = ["...", "dynamic-metadata"]
build-backend = "..."

[project]
dynamic = ["version"]

[[tool.dynamic-metadata]]
provider = "dynamic_metadata.regex"
field = "version"
input = "src/my_package/__init__.py"

dynamic_metadata.regex is the registered name of the bundled plugin. Since it lives inside dynamic-metadata, you have to include that in your requirements. Make sure the field is marked dynamic in your project table. The settings are defined by the plugin; see Bundled plugins for the full list of settings each one accepts.

Providing a custom plugin

You don’t have to publish a plugin, or even put it in an installed package, to use one. Give provider as an inline table with a path (a local directory) and a module (imported from it), so a plugin can live right inside your project alongside pyproject.toml. A provider loaded this way needs no runtime dependency on dynamic-metadata — it just has to expose the hooks.

Drop a module in your project — say scripts/my_plugin.py:

def dynamic_metadata(settings, project):
    return {"version": "1.2.3"}

and point an entry at it:

[project]
dynamic = ["version"]

[[tool.dynamic-metadata]]
provider = { path = "scripts", module = "my_plugin" }

module is the module name (my_plugin, the file without its .py), and path is the directory to find it in (relative to pyproject.toml). It must be an existing directory, and it is searched in isolation: a module of the same name reachable elsewhere on sys.path will not shadow or substitute for one missing from path. Use module = "my_plugin:MyClass" to load a class — it is imported and instantiated the same way.

Because the module is imported, make sure any third-party packages it needs are available at build time. If they aren’t already pulled in by your build backend, list them in [build-system].requires, or have the plugin declare them from its get_requires_for_dynamic_metadata hook (see plugin authors).

Inspecting the result

Installing dynamic-metadata provides a dynamic-metadata command (also runnable as python -m dynamic_metadata) for previewing what your plugins produce, without invoking the build backend:

$ dynamic-metadata show

show reads pyproject.toml from the current directory, runs the configured [[tool.dynamic-metadata]] entries in order, and prints the resolved [project] table as JSON. Use --pyproject-toml PATH to point at another file and --state to choose the build state passed to plugins (default metadata_wheel).

dynamic-metadata providers lists the provider names registered in your environment (the bundled plugins plus any installed third-party plugins) and the module each resolves to.

Mixing static and dynamic values (PEP 808)

Following PEP 808, list and table fields can be given a static value in [project] and listed in dynamic at the same time. A provider may only add to the static portion — it cannot remove, reorder, or change existing entries.

[project]
dependencies = ["torch", "packaging"]
dynamic = ["dependencies"]

[[tool.dynamic-metadata]]
provider = "..."
field = "dependencies"

The provider returns only its additions; the loader merges them onto the current value, with existing entries kept first and the provider’s entries appended verbatim. A provider may read the value of any field already resolved (the static value of the field it is extending, or any field produced by an earlier entry) via project[...] to decide what to add; reading a field that has not been produced yet raises a KeyError. For tables (urls, scripts, entry-points, optional-dependencies, …) the provider may add keys but not change the value of an existing one.

This add-only merge applies to every list/table field. The single-value fields (version, description, requires-python, license, and readme) cannot be extended, so they may not be both static and dynamic; a later entry targeting one of them instead replaces the value (a transform pipeline — for example, one plugin extracts a version and a later one normalizes it).