Introducing pyprojectr
A Type-Safe Way to Work with pyproject.toml
Python packaging has improved dramatically over the last several years. The shift toward pyproject.toml as the single source of truth for project metadata was a clear step forward. Most of the time, however, we still treat that file as a loosely structured dictionary.
That works… until it doesn’t.
When you start writing tools that need to reliably read, validate, or transform project metadata — especially once custom [tool.*] sections enter the picture — the cracks appear quickly. You end up with a lot of defensive .get() chains, special-casing for fields that can be either a string or a table, and repeated logic for converting between hyphenated TOML keys and Python attribute names.
I got tired of rewriting that code. So I wrote pyprojectr.
What pyprojectr does
pyprojectr is a small library that turns a pyproject.toml file into well-typed Python objects. It understands the standard [project] and [build-system] tables, handles the common variations in field shapes, and makes it straightforward to model your own tool-specific configuration.
The design goals were simple:
- Prefer explicit, typed models over dictionaries
- Respect the actual shapes defined in the packaging standards
- Make the hyphenated ↔ underscored conversion automatic (but controllable)
- Stay out of the way when you need to support custom tool tables
Basic usage
Loading a project is intentionally straightforward:
from pathlib import Path
from pyprojectr import pyproject
pyproj = pyproject.from_file(Path("pyproject.toml"))
print(pyproj.project.name)
print(pyproj.project.version)
print(pyproj.project.dependencies)
You get proper attributes instead of nested dictionaries. Fields that the standard allows in multiple forms (such as readme) are normalized into consistent Python types.
You can also construct models programmatically when you need to generate or transform configuration:
from pyprojectr import PyProject, Author
project = PyProject(
name="my-awesome-project",
version="0.1.0",
authors=[Author(name="Jane Doe", email="[email protected]")],
)
The interesting part: custom tool tables
Most real-world pyproject.toml files contain more than just the standard tables. Tools store their configuration under [tool.<name>]. Many of those sections are only lightly documented, and almost none of them come with a typed model.
pyprojectr treats this as a first-class concern. You define a model by subclassing PyProjectTool (built on attrs), and the library handles the key conversion for you:
import attrs
from pyprojectr.core import PyProjectTool
@attrs.define(frozen=True)
class MyCustomTool(PyProjectTool):
api_key: str
max_retries: int = 3
enable_logging: bool = True
Given a dictionary that came from a [tool.my-custom-tool] section:
[build-system]
...
[tool.my-custom-tool]
api-key = "secret-token"
max-retries = 3
enable-logging = falsefrom pyprojectr import pyproject
pyproj = pyproject.from_file("pyproject.toml")
my_tool = pyproj.get_tool_options("my-custom-tool", MyCustomTool)
print(my_tool.api_key) # secret-token
print(my_tool.max_retries) # 5
Going the other direction is equally simple:
unstructured = my_tool.to_data()
# {"api-key": "secret-token", "max-retries": 5, "enable-logging": False}
If you need to keep the original key names (or disable renaming for an entire class), you can opt out with a small amount of metadata. The defaults are designed for the common case; the escape hatches exist for the uncommon ones.
The library already ships with models for a couple of frequently used tools (pytest and setuptools-scm). Adding more is intentionally low-friction.
Why this exists
I maintain a few packaging-related tools, including a setuptools-scm plugin. Working on those projects made the friction of ad-hoc pyproject.toml handling very visible. The same patterns kept showing up:
- Checking whether a field was a string or a table
- Converting between
requires-pythonandrequires_python - Reaching into nested tool tables without any type information
- Writing the same validation and defaulting logic in multiple places
None of these problems are particularly hard in isolation. Collectively they become a tax on every packaging or release-related tool you write. pyprojectr is an attempt to pay that tax once and then move on.
It is deliberately focused. It does not try to be a full project manager, a dependency resolver, or a replacement for the packaging standards themselves. It just gives you a solid, typed foundation to build on top of.
Current status and next steps
pyprojectr is available on PyPI:
pip install pyprojectr
# or
uv add pyprojectr
The core modeling for [project] and [build-system] is solid, the extension mechanism for custom tools works well, and the test suite covers the common edge cases. There is still room to grow — more built-in tool models, richer validation, better error messages, and improved documentation are all on the list.
If you write tools that need to understand pyproject.toml, I would be interested in hearing what custom sections you care about most. The easiest way to extend the library is often just defining a small model and contributing it upstream.
Links
Feedback, issues, and pull requests are welcome.