Skip to content

Pydantic

Pydantic models are typed by the plugin and still validated by pydantic at run time.

Run it

python  pydantic_models.ppy
ppy     pydantic_models.ppy
ppy run pydantic_models.ppy

Two shapes, kept apart

A pydantic model has a constructor shape and a validated output shape, and the plugin distinguishes them. User(id="123", name="ada") is legal because pydantic coerces the string. user.id + 1 is int arithmetic, because the checker types the field from the model, not from the argument. Validation still runs as pydantic wrote it.

Constraints become facts

class Pixel(BaseModel):
    r: Annotated[int, Field(ge=0, le=255)]
    g: Annotated[int, Field(ge=0, le=255)]
    b: Annotated[int, Field(ge=0, le=255)]


@ppy.pure
def luminance(pixel: Pixel) -> float:
    return 0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b

Field(ge=0, le=255) is read as an integer range as well as a validator, the same refinement ppy.Range(0, 255) gives. Inside luminance the three fields are known to fit a byte, and the arithmetic needs no overflow guard.

These spellings all count:

  • Annotated[int, Field(...)]
  • count: int = Field(ge=0, le=100)
  • conint

Schema building at build time

Schema building is code the model runs at import. It falls under the same policy as JAX export: [tool.ppy] build-execution decides whether a build may execute project code, and the default is deny.

What it prints

python pydantic_models.ppy, ppy pydantic_models.ppy, ppy run pydantic_models.ppy

145.7586 124 ada

Read on: Plugins: Pydantic ยท Uvicorn and FastAPI

pydantic_models.ppy is hand-written; there is no .py source and no conversion step.

06_pydantic/pydantic_models.ppy

from typing import Annotated

import ppy
from pydantic import BaseModel, Field


class Pixel(BaseModel):
    r: Annotated[int, Field(ge=0, le=255)]
    g: Annotated[int, Field(ge=0, le=255)]
    b: Annotated[int, Field(ge=0, le=255)]


class User(BaseModel):
    id: int
    name: str


@ppy.pure
def luminance(pixel: Pixel) -> float:
    return 0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b


def main() -> None:
    pixel = Pixel(r=255, g=128, b=0)
    user = User(id="123", name="ada")
    print(round(luminance(pixel), 4), user.id + 1, user.name)


if __name__ == "__main__":
    main()

Source: examples/06_pydantic.