Pydantic Boundary Models
model Foo: emits a Pydantic BaseModel(extra="forbid"). Use it for data crossing a trust boundary — HTTP requests, JSON files, env-derived structures, message-queue payloads — where you want runtime validation in addition to compile-time types.
A worked example
model UserInput: email: str name: str = "anon" age: int? = Noneclass UserInput(BaseModel): model_config = ConfigDict(extra="forbid")
email: str name: str = "anon" age: int | None = NoneWhat you get
- Compile-time type-checking — Typhon’s static rules apply.
- Runtime validation — Pydantic checks types and rejects on construction.
extra="forbid"— unexpected fields raiseValidationError, not silently drop.@dataclass-style construction —UserInput(email="a@b")..model_dump()/.model_validate()— for JSON serialisation. Since v0.10.0 these (plus.model_dump_json()) also work under the in-process VM (tyc run) for flatmodelclasses:model_validate(mapping)constructs an instance from a dict,model_dump()returns the fields in declaration order, andmodel_dump_json()the JSON form. Nested-model validation is not type-directed in the VM yet — deeply-nested models still need--compile.
When to use model vs class
class (dataclass) | model (Pydantic) |
|---|---|
| Internal types | Data crossing trust boundaries |
| Maximum performance | Runtime validation |
| No extra constraints | Field(min_length=1, ...) validators |
Mix freely in one project.
With Pydantic field validators
from pydantic import Field
model UserInput: email: str = Field(..., pattern=r"^[^@]+@[^@]+$") age: int = Field(..., ge=0, le=150)Field(...) is Pydantic — the checker treats it as Pydantic-equivalent and the runtime validator enforces. Typhon does not introduce its own validator surface; Pydantic’s is mature.
In FastAPI / similar frameworks
from fastapi import FastAPI
app = FastAPI()
model CreateUser: email: str name: str
model UserResponse: id: int email: str name: str
@app.post("/users", response_model=UserResponse)async def create_user(input: CreateUser) -> UserResponse: ...model plays exactly as Pydantic does — FastAPI sees the BaseModel and binds it to the request body.
Why extra="forbid" is the default
Pydantic’s stock setting is extra="ignore", which silently drops unexpected fields. Typhon’s safety pitch forbids quiet failures: if a client sends {"email": "a@b", "extra": "boom"}, you want a 422 response, not a silent drop.
If you genuinely need permissive mode, the configurable [emit] model-extra knob is roadmapped. There is no Typhon syntax for overriding the per-class model_config today — edit the emitted .py by hand, write the model as a plain Pydantic class in a .py file alongside your .ty sources (the build pipeline copies plain .py through untouched), or wait for the config knob to land.
Frozen models
The frozen modifier currently only applies to class (see Classes &
Models) — model Foo frozen: is not parsed
today. To make a Pydantic model immutable at runtime, drop into a
plain .py file alongside your .ty sources (the build pipeline
copies plain .py through):
from pydantic import BaseModel, ConfigDict
class Coordinate(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True)
x: float y: floatPydantic’s frozen=True blocks field reassignment (faux immutability —
nested mutables can still be mutated). A model modifier for frozen
is on the roadmap.
Where next
- Classes and Models (tour) —
classvsmodel. class,model,class!reference — declaration forms.- HTTP API recipe — worked Pydantic + FastAPI pattern.