This release is a pre-release and may not be stable for production use.
Typed views for Pydantic models
pydantic-views lets you derive focused, type-safe Pydantic models (“views”) from a base model. Each view exposes only the fields appropriate for a given operation—create, update, load, or a custom flow—so you avoid hand-maintaining parallel schemas.
You annotate each field of your base model once with how it may be accessed (read-only, write-only, hidden, …), and pydantic-views generates the right model for every operation. Nested and recurrent models get their matching views automatically, validators and field metadata are preserved, and the result is fully typed.
Typical service signatures become easy to express:
class ExampleModelCreate(View[ExampleModel], preset=CreatePreset):
pass
class ExampleModelCreateResult(View[ExampleModel], preset=CreateResultPreset):
pass
class ExampleModelLoad(View[ExampleModel], preset=LoadPreset):
pass
class ExampleModelUpdate(View[ExampleModel], preset=UpdatePreset):
pass
def create(input: ExampleModelCreate) -> ExampleModelCreateResult: ...
def load(model_id: str) -> ExampleModelLoad: ...
def update(model_id: str, input: ExampleModelUpdate) -> ExampleModelLoad: ...
Features
Unlimited views per model (create, update, load, or any custom flow).
Declare field access once; every view is derived from a single source of truth.
Works on nested models; referenced (and recurrent) models get their views too.
Works on RootModel and complex container types (list, set, tuple, dict, Literal, unions).
Ready-made presets and builders for common patterns, or define views manually.
Preserves validators, aliases, defaults, titles, descriptions and other field metadata.
Custom access tags for fine-grained, per-view field selection.
Helpers to build a view from a model, build a model from a view, and merge a view into a model.
Fully typed with a shipped py.typed marker and an extensive test suite.
Static type checking via a bundled mypy plugin and a .pyi stub generator for other type checkers.
Open source and published on PyPI.
Installation
Using pip:
pip install pydantic-views
Using poetry:
poetry add pydantic-views
Using uv:
uv add pydantic-views
Quickstart
Mark each field with its access mode using the provided annotations. Unmarked fields default to read/write everywhere.
from typing import Annotated
from annotated_types import Gt
from pydantic import BaseModel, computed_field
from pydantic_views import AccessMode, Hidden, ReadOnly, ReadOnlyOnCreation
class ExampleModel(BaseModel):
# Unmarked fields are read/write everywhere.
field_str: str
# Read-only fields are removed from create and update views.
field_read_only_str: ReadOnly[str]
# Read-only-on-creation fields are hidden on create, update and load views,
# but appear on create-result views.
field_api_secret: ReadOnlyOnCreation[str]
# Combine access modes with Annotated and keep validators (Gt in this case).
field_int: Annotated[int, AccessMode.READ_ONLY, AccessMode.WRITE_ONLY_ON_CREATION, Gt(5)]
# Hidden fields never appear.
field_hidden_int: Hidden[int]
# Computed fields appear only on read views.
@computed_field
def field_computed_field(self) -> int:
return self.field_hidden_int * 5
Access modes
Every field carries one or more access modes. The mode decides which generated views expose the field. You can use the convenient annotation aliases (ReadOnly[T], WriteOnly[T], …) or attach AccessMode values directly with Annotated when you want to combine several modes or keep extra validators.
Annotation |
Access mode |
Create |
CreateResult |
Update |
Load |
|---|---|---|---|---|---|
(unmarked) / ReadAndWrite[T] |
READ_AND_WRITE |
✓ |
✓ |
✓ |
✓ |
ReadOnly[T] |
READ_ONLY |
✓ |
✓ |
||
WriteOnly[T] |
WRITE_ONLY |
✓ |
✓ |
||
ReadOnlyOnCreation[T] |
READ_ONLY_ON_CREATION |
✓ |
|||
WriteOnlyOnCreation[T] |
WRITE_ONLY_ON_CREATION |
✓ |
|||
Hidden[T] |
HIDDEN |
||||
@computed_field |
✓ |
✓ |
Create also hides default None values, Update makes every field optional, and the read views (CreateResult and Load) include computed fields.
The four standard views
pydantic-views ships four presets that cover the typical CRUD lifecycle. Each preset is a Preset (a NamedTuple) you pass to a View subclass via preset=:
CreatePreset — input accepted when creating a resource (writable and write-on-creation fields).
CreateResultPreset — what you return after creation (readable fields plus computed fields).
UpdatePreset — partial input for updates (writable fields, all optional).
LoadPreset — what you return when reading a resource (readable fields plus computed fields).
Build a load view
from pydantic_views import View, LoadPreset
class ExampleModelLoad(View[ExampleModel], preset=LoadPreset):
pass
Which is equivalent to:
from typing import Annotated
from annotated_types import Gt
from pydantic import BaseModel
from pydantic_views import View
class ExampleModelLoad(BaseModel):
field_str: str
field_int: Annotated[int, Gt(5)]
field_read_only_str: str
field_computed_field: int
Build an update view
from pydantic_views import View, UpdatePreset
class ExampleModelUpdate(View[ExampleModel], preset=UpdatePreset):
pass
Which is equivalent to:
from pydantic import Field, BaseModel
from pydantic_core import MISSING
from pydantic_views import View
class ExampleModelUpdate(BaseModel):
field_str: str = Field(default_factory=lambda: MISSING)
On Update views every field uses a default factory that returns MISSING, so fields become optional. Applying the view to a model only updates values that were set.
original_model = ExampleModel(
field_str="anything",
field_read_only_str="anything",
field_api_secret="anything",
field_int=10,
field_hidden_int=33,
)
update = ExampleModelUpdate(field_str="new_data")
updated_model = update.view_apply_to(original_model)
assert isinstance(updated_model, ExampleModel)
assert updated_model.field_str == "new_data"
If a field is not set on the update view, the original value is kept.
original_model = ExampleModel(
field_str="anything",
field_read_only_str="anything",
field_api_secret="anything",
field_int=10,
field_hidden_int=33,
)
update = ExampleModelUpdate()
updated_model = update.view_apply_to(original_model)
assert isinstance(updated_model, ExampleModel)
assert updated_model.field_str == "anything"
Working with view data
Every generated view inherits a small set of helpers from View to move data between the base model and its views:
View.view_build_from(model) — build a view instance from a model instance, omitting unset fields.
view.view_build_to() — build a base-model instance from the view, using only the fields set on the view.
view.view_apply_to(model) — return a copy of model updated with the fields set on the view (deep merge).
View.view_class_root() — return the base model class the view was generated from.
# Project a stored model into the read view returned by your API.
stored = ExampleModel(
field_str="value",
field_read_only_str="ro",
field_api_secret="secret",
field_int=10,
field_hidden_int=33,
)
payload = ExampleModelLoad.view_build_from(stored)
# Apply a partial update and get a new, validated model back.
patched = ExampleModelUpdate(field_str="updated").view_apply_to(stored)
Two ways to define your views
Subclass View (with a preset)
The declarative form integrates with type checkers and IDEs, and lets you add extra fields:
from pydantic_views import View, CreatePreset
class ExampleModelCreate(View[ExampleModel], preset=CreatePreset):
# You can add fields that are not part of the base model.
extra_flag: bool = False
The preset= keyword expands the preset’s configuration into the view; an explicit keyword passed alongside it (for example view_name=...) overrides the preset’s value. The bundled mypy plugin understands both the preset= form and explicit keywords. (The older **LoadPreset._asdict() splat still works at runtime, but mypy cannot analyse ** unpackings in class keywords, so prefer preset= for full type checking.)
You can also pass the configuration directly as keyword arguments instead of a preset:
from pydantic_views import AccessMode, View
class ExampleModelReadOnly(
View[ExampleModel],
view_name="ReadOnly",
access_modes=(AccessMode.READ_AND_WRITE, AccessMode.READ_ONLY),
include_computed_fields=True,
):
pass
Use a builder
The imperative form builds (and caches) a view class on demand—handy when generating views dynamically:
from pydantic_views import BuilderLoad, BuilderUpdate
ExampleModelLoad = BuilderLoad().build_view(ExampleModel)
ExampleModelUpdate = BuilderUpdate().build_view(ExampleModel)
Views are cached per base model, so building the same view twice returns the same class.
Custom views with builders
For anything beyond the standard presets, build your own Builder. The configuration is shared by builders, presets and the View subclass keyword arguments:
access_modes — which access modes to include in the view.
include_tags / exclude_tags — force-include or force-exclude fields by AccessTag.
all_optional — make every field optional (the basis of Update views).
all_nullable — make every field nullable.
hide_default_null — drop default None values so they don’t appear in the schema.
include_computed_fields — include @computed_field properties.
from pydantic_views import AccessMode, Builder
builder = Builder(
view_name="Summary",
access_modes=(AccessMode.READ_AND_WRITE, AccessMode.READ_ONLY),
include_computed_fields=True,
all_nullable=True,
)
ExampleModelSummary = builder.build_view(ExampleModel)
Nested models
When a field references another Pydantic model, pydantic-views generates a matching view for it automatically, recursively, and even for self-referential or circular models. Container types (list, set, tuple, dict, Literal and unions) are traversed too.
from pydantic import BaseModel
from pydantic_views import ReadOnly, View, LoadPreset
class Address(BaseModel):
street: str
zip_code: ReadOnly[str]
class User(BaseModel):
name: str
addresses: list[Address]
class UserLoad(View[User], preset=LoadPreset):
pass
# UserLoad.addresses is a list of the generated AddressLoad view.
Static type checking
Views are generated at runtime, so a type checker cannot see their fields on its own. pydantic-views ships a mypy plugin (pydantic_views.mypy) that reproduces the builder’s field selection statically. Enable it in your mypy configuration, before pydantic.mypy:
[mypy]
plugins = pydantic_views.mypy, pydantic.mypy
It understands both the preset= and explicit-keyword forms of a view, and types nested-model fields with their generated views (Address + Load -> AddressLoad):
from pydantic_views import LoadPreset, View
class UserLoad(View[User], preset=LoadPreset):
pass
reveal_type(UserLoad.model_validate({}).id) # int — the plugin knows the field exists
UserLoad(password="secret") # error: write-only field is not in a load view
For type checkers that don’t run mypy plugins (pyright, Pylance, PyCharm, …), pydantic-views ships a stub generator. It imports your module and writes a .pyi describing every view’s real fields (plus the regular classes, models and functions around them):
python -m pydantic_views.stubgen myapp.models
Pass a package name to stub every submodule, list several modules at once, and use -o/--output-dir to write the stubs to a separate tree. See Type checking and stub files for the stub generator and mypy plugin options, and the full list of what is and isn’t analysed statically.
Documentation
Full documentation, including the complete API reference, is available at pydantic-views.readthedocs.io.
Metadata
Release files for pydantic-views 0.9.0b0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pydantic_views-0.9.0b0.tar.gz | 29.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pydantic_views-0.9.0b0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 61.9 kB
Release files / pydantic_views-0.9.0b0.tar.gz
| Download URL | pydantic_views-0.9.0b0.tar.gz |
|---|---|
| Size | 29.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9887ede69ba116efe843d0503a3a0686f71259ba5bf627fa26e543734b7c51ca
|
|
BLAKE2b-256 checksum How to use checksums |
b28f9b2cddfa340be89db432abb1db79ce7dc39a8012f53eb87e3b1ac9354525
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logRelease files / pydantic_views-0.9.0b0-py3-none-any.whl
| Download URL | pydantic_views-0.9.0b0-py3-none-any.whl |
|---|---|
| Size | 32.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
446a5073139e658b04ae9427c965a3ee6c000c7b9cc39ce107801c71af6905d3
|
|
BLAKE2b-256 checksum How to use checksums |
46aa477fc243f250a2095eb55a2f3384dd35939eeba074b7badb9db5398c744c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency log