Examples

Minimal create/load/update

Build common views from a single model and use them as function signatures.

from pydantic import BaseModel
from pydantic_views import BuilderCreate, BuilderCreateResult, BuilderLoad, BuilderUpdate

class User(BaseModel):
    id: int
    email: str
    password: str

UserCreate = BuilderCreate().build_view(User)
UserCreateResult = BuilderCreateResult().build_view(User)
UserLoad = BuilderLoad().build_view(User)
UserUpdate = BuilderUpdate().build_view(User)

def create_user(input: UserCreate) -> UserCreateResult: ...
def get_user(user_id: int) -> UserLoad: ...
def update_user(user_id: int, input: UserUpdate) -> UserLoad: ...

Declarative views with presets

Prefer the declarative form when you want the views to be fully type-checked: subclass View and pass a preset via preset=. The bundled mypy plugin understands this form (unlike the **Preset._asdict() splat), and you can still declare extra fields that live only on the view.

from pydantic import BaseModel
from pydantic_views import CreatePreset, CreateResultPreset, LoadPreset, UpdatePreset, View

class User(BaseModel):
    id: int
    email: str
    password: str

class UserCreate(View[User], preset=CreatePreset):
    # A field that only exists on the view, not on the base model.
    accept_terms: bool = False

class UserCreateResult(View[User], preset=CreateResultPreset):
    pass

class UserLoad(View[User], preset=LoadPreset):
    pass

class UserUpdate(View[User], preset=UpdatePreset):
    pass

def create_user(input: UserCreate) -> UserCreateResult: ...
def get_user(user_id: int) -> UserLoad: ...
def update_user(user_id: int, input: UserUpdate) -> UserLoad: ...

Declarative views without presets

You do not need a preset: pass the configuration directly as keyword arguments to build any custom view. The recognised keywords are view_name, access_modes, all_optional, all_nullable, hide_default_null and include_computed_fields.

from pydantic import BaseModel, computed_field
from pydantic_views import AccessMode, ReadOnly, WriteOnly, View

class Account(BaseModel):
    id: ReadOnly[int]
    username: str
    password: WriteOnly[str]

    @computed_field
    def handle(self) -> str:
        return f"@{self.username}"

# A bespoke "public" read view spelled out with explicit keywords (no preset).
class AccountPublic(
    View[Account],
    view_name="Public",
    access_modes=(AccessMode.READ_AND_WRITE, AccessMode.READ_ONLY),
    include_computed_fields=True,
):
    pass
# -> id, username, handle   (write-only ``password`` is excluded)

Filtering fields with tags

Tag fields with AccessTag to force-include or force-exclude them regardless of the view’s access modes. exclude_tags drops tagged fields that would otherwise be kept; include_tags pulls in tagged fields that the access modes would otherwise leave out.

from typing import Annotated

from pydantic import BaseModel
from pydantic_views import AccessMode, AccessTag, View

class Article(BaseModel):
    title: str
    body: str
    # Read-only, but only meant for staff:
    moderation_notes: Annotated[str, AccessMode.READ_ONLY, AccessTag("admin")]
    view_count: Annotated[int, AccessMode.READ_ONLY, AccessTag("stats")]
    # Write-only secret used to build a preview link:
    draft_token: Annotated[str, AccessMode.WRITE_ONLY, AccessTag("preview")]

# Public view: keep readable fields, but drop anything tagged "admin".
class ArticlePublic(
    View[Article],
    view_name="Public",
    access_modes=(AccessMode.READ_AND_WRITE, AccessMode.READ_ONLY),
    exclude_tags=(AccessTag("admin"),),
):
    pass
# -> title, body, view_count   (moderation_notes dropped by its tag)

# Preview view: readable fields plus the write-only ``draft_token`` pulled in by its tag.
class ArticlePreview(
    View[Article],
    view_name="Preview",
    access_modes=(AccessMode.READ_AND_WRITE, AccessMode.READ_ONLY),
    include_tags=(AccessTag("preview"),),
):
    pass
# -> title, body, moderation_notes, view_count, draft_token

AccessTag instances are interned by name (AccessTag("preview") is AccessTag("preview")), so the same tag object is reused everywhere you reference it.

Nested models and selective fields

Views cascade into nested models and respect access modes you annotate.

from typing import Annotated

from pydantic import BaseModel, computed_field
from pydantic_views import AccessMode, ReadOnly, ReadOnlyOnCreation, BuilderLoad

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class Profile(BaseModel):
    username: str
    email: ReadOnly[str]
    # Hide on create/update, show after creation
    api_token: ReadOnlyOnCreation[str]
    # Expose only when reading (load views)
    score: Annotated[int, AccessMode.READ_ONLY]

    @computed_field
    def location(self) -> str:
        return f"{self.username} @ {self.email}"

    address: Address

ProfileLoad = BuilderLoad().build_view(Profile)

profile = Profile(
    username="alice",
    email="alice@example.com",
    api_token="secret",
    score=42,
    address=Address(street="Main", city="Springfield", zip_code="00000"),
)

loaded = ProfileLoad.view_build_from(profile)
assert loaded.address.city == "Springfield"
# api_token and score are present, write-only fields would have been stripped

Applying partial updates

Update views accept only the fields you want to change and merge them into an instance.

from pydantic import BaseModel
from pydantic_views import BuilderUpdate

class Settings(BaseModel):
    theme: str
    locale: str
    marketing_opt_in: bool

SettingsUpdate = BuilderUpdate().build_view(Settings)

current = Settings(theme="light", locale="en", marketing_opt_in=False)
patch = SettingsUpdate(theme="dark")

updated = patch.view_apply_to(current)

assert updated.theme == "dark"
assert updated.locale == "en"  # unchanged

Create + result pair with computed fields

Use BuilderCreate to accept input and BuilderCreateResult to return read-only and computed fields.

from pydantic import BaseModel, computed_field
from pydantic_views import BuilderCreate, BuilderCreateResult, ReadOnly

class Invoice(BaseModel):
    id: ReadOnly[int]
    description: str
    units: int
    unit_price: float

    @computed_field
    def total(self) -> float:
        return self.units * self.unit_price

InvoiceCreate = BuilderCreate().build_view(Invoice)
InvoiceCreateResult = BuilderCreateResult().build_view(Invoice)

new_invoice = InvoiceCreate(description="Hosting", units=2, unit_price=25.0)
stored = Invoice(id=1, **new_invoice.model_dump())

result = InvoiceCreateResult.view_build_from(stored)
assert result.total == 50.0

Custom builder with nullable fields

Craft a bespoke builder to expose only certain access modes and make every field optional and nullable — a patch-style view where you set just the fields you want to change.

from pydantic import BaseModel
from pydantic_views import Builder, AccessMode

SoftDeleteBuilder = Builder(
    view_name="SoftDelete",
    access_modes=(AccessMode.READ_AND_WRITE,),
    all_optional=True,
    all_nullable=True,
)

class Document(BaseModel):
    title: str
    body: str
    deleted_at: float | None = None

DocumentSoftDelete = SoftDeleteBuilder.build_view(Document)

doc = Document(title="Plan", body="...")
soft_delete = DocumentSoftDelete(deleted_at=1720000000.0)

deleted_doc = soft_delete.view_apply_to(doc)
assert deleted_doc.deleted_at is not None