Type checking and stub files¶
Views are generated at runtime, so a type checker cannot see their fields just by reading your
source. pydantic-views ships a mypy plugin (pydantic_views.mypy) that reproduces the
builder’s field selection statically — it synthesises the right attributes and __init__
signature for every view. For type checkers other than mypy (pyright, Pylance, PyCharm, …) you can
turn the plugin’s output into stub files so they become aware of the same fields.
Configuring the mypy plugin¶
Enable the plugin in your mypy configuration. It must be listed before pydantic.mypy —
base-class hooks are first-wins, and the pydantic-views plugin needs to run first:
; mypy.ini / setup.cfg
[mypy]
plugins = pydantic_views.mypy, pydantic.mypy
The same works from pyproject.toml:
[tool.mypy]
plugins = ["pydantic_views.mypy", "pydantic.mypy"]
That is all that is required. Once enabled, both the preset= form and the explicit-keyword form
of a view are understood:
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
Plugin options¶
The plugin reads a single option from an [pydantic-views-mypy] section:
[mypy]
plugins = pydantic_views.mypy, pydantic.mypy
[pydantic-views-mypy]
; Whether the synthesised __init__ should reject unknown keyword arguments. Default: true.
init_forbid_extra = true
Note
init_forbid_extra is read only from INI-style configuration (mypy.ini /
setup.cfg). When mypy is configured from pyproject.toml the option is not parsed and
falls back to its default (true). The plugins entry itself works from either format; only
this option requires an INI file to override.
What the plugin understands¶
The
preset=<Preset>form — resolved to the keywords of the referencedPreset(...)definition — and the explicit-keyword form (view_name=...,access_modes=(...),all_optional,all_nullable,include_computed_fields). An explicit keyword passed alongsidepresetoverrides the preset’s value.Nested models, recursively and through
list/set/tuple/dict/ unions: a field referencing another model is typed with that model’s generated view (Address+Load->AddressLoad).
A few things cannot be recovered statically:
The
**LoadPreset._asdict()splat — mypy discards**unpackings in class keywords. Usepreset=LoadPresetor explicit keywords instead.Field-level
AccessTagfiltering (include_tags/exclude_tags): the tags live inAnnotatedruntime values that mypy drops.A view must be declared in the same module as its source model, because mypy frees the annotations of imported modules.
These limits apply to the mypy plugin only. The runtime stub generator described below is not subject to any of them, because it reads the views’ real fields after import.
Generating stub files for other type checkers¶
Type checkers that do not run mypy plugins (pyright, Pylance, PyCharm) see a view as an empty model
with an __init__(**data: Any) signature. Plain stubgen does not help either: it does not
execute mypy plugins, so it produces the same empty view.
pydantic-views ships its own stub generator, pydantic_views.stubgen, that solves this without
mypy. It imports the target module, inspects the views at runtime, and writes a .pyi stub
describing their real fields. Run it as a module, passing the importable name of the module to stub:
$ python -m pydantic_views.stubgen myapp.models
wrote /path/to/myapp/models.pyi
By default the stub is written next to its source file (models.py -> models.pyi). Pass a
package name to walk every submodule, list several modules at once, and use -o /
--output-dir to mirror the package tree into a separate directory instead of writing the stubs in
place:
$ python -m pydantic_views.stubgen myapp.models myapp.schemas --output-dir build/stubs
Wire either command into a pre-commit hook or a make target to keep the stubs in sync with your
models.
What the stub contains¶
Because the generator works from the imported module, each stub is a complete description of that module, not just its views:
every regular class — plain classes, enums (including ones nested inside a model), and Pydantic models, with
@computed_fieldproperties rendered as read-only properties;every view declared in the module;
every view generated at runtime, including the nested views built for referenced models (
Address+Load->AddressLoad);module-level functions, preserving PEP 695 type parameters.
Each view is emitted with its real field set and a keyword-only __init__, so other type checkers
resolve attributes and constructor calls exactly as the mypy plugin would. For a model like:
class Account(BaseModel):
id: ReadOnly[int]
username: str
password: WriteOnly[str]
class AccountLoad(View[Account], preset=LoadPreset):
pass
the generator emits both the model and the view:
class Account(BaseModel):
id: int
username: str
password: str
def __init__(self, *, id: int, username: str, password: str) -> None: ...
class AccountLoad(View[Account]):
id: int
username: str
def __init__(self, *, id: int, username: str) -> None: ...
Reproducing the whole module is deliberate: a type checker treats an adjacent <module>.pyi as the
complete description of the module and ignores the .py, so a stub that listed only the views would
hide every other symbol. Because the generator emits the regular classes, models, functions and views
together, you can point it at any module — there is no need to isolate views in a dedicated module.
Because it reflects the runtime model rather than a static analysis, the generator also captures the
views the mypy plugin cannot recover: those built with the **Preset._asdict() splat, views
declared in a different module from their source model, and
AccessTag include_tags / exclude_tags filtering.
A runnable reference that stubs the bundled example models and verifies the generated field sets
against the runtime model_fields lives in
examples/generate_stubs.py.