Pydantic
Pydantic checks data against type hints. In a desktop app it does two jobs: it validates what the user typed in a form, and it parses JSON from files and APIs. It runs in the same process as the window, with no thread or loop needed, because validation is fast.

cd examples/integrations/pydantic_signupuv run main.pycd examples/integrations/pydantic_signuppython3 -m venv .venvsource .venv/bin/activatepip install pyzed-ui "pydantic[email]"python main.pycd examples/integrations/pydantic_signuppy -m venv .venv.venv\Scripts\Activate.ps1pip install pyzed-ui "pydantic[email]"python main.pycd examples/integrations/pydantic_signuppython3 -m pip install pyzed-ui "pydantic[email]"python3 main.pycd examples/integrations/pydantic_signuppy -m pip install pyzed-ui "pydantic[email]"py main.py-
Write the rules as a model. Fields carry the constraints, special types like
EmailStr(frompydantic[email]) andSecretStrdo the rest, and validators add custom checks. Anaftermodel validator compares two fields:class SignUp(BaseModel):username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$")email: EmailStrbirthday: dateplan: Literal["free", "pro", "team"] = "free"password: SecretStrpassword_repeat: SecretStrnewsletter: bool = False@field_validator("username", mode="before")@classmethoddef lowercase(cls, value: str) -> str:return value.strip().lower()@model_validator(mode="after")def passwords_match(self) -> "SignUp":if self.password.get_secret_value() != self.password_repeat.get_secret_value():raise ValueError("The passwords don't match")return self -
Keep the form as plain values and validate on submit. Inputs give strings; Pydantic converts
"1990-05-02"to adateand reports every problem at once:def submit(self, *_):try:signup = SignUp.model_validate(self.draft)except ValidationError as error:self.errors = pz.validation_errors(error)return... -
Show each error under its field.
pz.validation_errorsreturns{field: message}; errors from a model validator have the key"":ui.field("Email", ui.input(d["email"], self.edit("email")), error=self.errors.get("email"))ui.callout(self.errors[""], tint="error") if self.errors.get("") else None -
Save and load JSON. Serialization with
model_dumpandmodel_dump_json, and aTypeAdapterfor a whole list:account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"}))Accounts = TypeAdapter(list[Account])FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2))accounts = Accounts.validate_json(FILE.read_bytes())
Parsing API responses
Section titled “Parsing API responses”The same models parse JSON from the network. Declare only the fields you use; Pydantic ignores the rest, converts dates and fails clearly if the API changes shape. From the PyPI explorer:
class Info(BaseModel): name: str version: str summary: str | None = None
class File(BaseModel): upload_time_iso_8601: datetime
class Package(BaseModel): info: Info releases: dict[str, list[File]]
package = Package.model_validate_json(response.content)Full code
Section titled “Full code”"""A sign-up form whose rules live in a Pydantic model; accounts are saved to accounts.json.
uv run main.py
The window keeps what the user types as plain strings and hands them to``SignUp.model_validate``: Pydantic converts them (a date, an email, a bool), checks every ruleand reports the errors per field, which ``pz.validation_errors`` turns into ``{field: message}``.Walkthrough: https://albakore.github.io/pyzed/integrations/pydantic/"""
from __future__ import annotations
from pathlib import Path
from pydantic import ValidationError
import pyzed as pzfrom pyzed import ui
from models import Account, Accounts, SignUp
FILE = Path(__file__).with_name("accounts.json")EMPTY = {"username": "", "email": "", "birthday": "", "plan": "free", "password": "", "password_repeat": "", "newsletter": False}
def load() -> list[Account]: return Accounts.validate_json(FILE.read_bytes()) if FILE.exists() else []
class SignUpForm(pz.App): title = "Sign up · Pydantic" size = (860, 600)
def __init__(self): self.draft = dict(EMPTY) self.errors: dict[str, str] = {} self.accounts = load() self.preview = "" # the JSON Pydantic produced for the last account
def edit(self, key): return lambda value: self.draft.__setitem__(key, value)
def submit(self, *_): try: signup = SignUp.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) # the model validator's error has the key "" return account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"})) if any(a.username == account.username for a in self.accounts): self.errors = {"username": "That username is taken"} return self.accounts.append(account) FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2)) self.preview = account.model_dump_json(indent=2) self.draft, self.errors = dict(EMPTY), {}
def form(self): e, d = self.errors.get, self.draft
def text(key, title, placeholder="", **kwargs): return ui.field(title, ui.input(d[key], self.edit(key), placeholder=placeholder, on_submit=self.submit, **kwargs), error=e(key))
return pz.column([ ui.heading("Create an account", level=2), text("username", "Username", "lowercase letters, digits and _"), text("email", "Email", "you@example.com"), text("birthday", "Birthday", "YYYY-MM-DD"), ui.field("Plan", ui.segmented(["free", "pro", "team"], d["plan"], self.edit("plan"))), text("password", "Password", "8 characters or more", secure=True), text("password_repeat", "Repeat the password", secure=True), ui.checkbox(d["newsletter"], self.edit("newsletter"), label="Send me the newsletter"), ui.callout(e(""), tint="error") if e("") else None, ui.button("Sign up", self.submit, icon="user-plus", variant="solid", full_width=True), ], spacing=12, padding=24, width=420)
def side(self, t): rows = [ui.list_item(a.username, None, icon="user", end=ui.badge(a.plan), inset=True, height=30) for a in reversed(self.accounts)] return pz.container(pz.column([ ui.muted("accounts.json", size="xs", mono=True), *rows, ui.card(ui.label(self.preview, mono=True, size="sm"), title="model_dump_json()") if self.preview else None, ], spacing=6), padding=16, width="fill", height="fill", bg=t.panel)
def view(self): t = self.resolved_theme() return pz.row([pz.container(pz.scrollable(self.form()), height="fill", bg=t.editor), self.side(t)])
if __name__ == "__main__": pz.run(SignUpForm)"""The form's rules, written as Pydantic models (https://docs.pydantic.dev/latest/concepts/models/)."""
from __future__ import annotations
from datetime import datefrom typing import Literal
from pydantic import BaseModel, EmailStr, Field, SecretStr, TypeAdapter, field_validator, model_validator
class SignUp(BaseModel): username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$") email: EmailStr birthday: date plan: Literal["free", "pro", "team"] = "free" password: SecretStr # repr() and str() show ******** password_repeat: SecretStr newsletter: bool = False
@field_validator("username", mode="before") @classmethod def lowercase(cls, value: str) -> str: return value.strip().lower()
@field_validator("birthday") @classmethod def old_enough(cls, value: date) -> date: today = date.today() age = today.year - value.year - ((today.month, today.day) < (value.month, value.day)) if age < 13: raise ValueError("You must be at least 13 years old") return value
@field_validator("password") @classmethod def long_enough(cls, value: SecretStr) -> SecretStr: if len(value.get_secret_value()) < 8: raise ValueError("Use 8 characters or more") return value
@model_validator(mode="after") def passwords_match(self) -> SignUp: if self.password.get_secret_value() != self.password_repeat.get_secret_value(): raise ValueError("The passwords don't match") return self
class Account(BaseModel): """What gets saved: the sign-up without the passwords."""
username: str email: EmailStr birthday: date plan: Literal["free", "pro", "team"] newsletter: bool
Accounts = TypeAdapter(list[Account]) # validates and serializes a whole list at once