feat: add mypy plugin for type-checking structs.replace kwargs#992
feat: add mypy plugin for type-checking structs.replace kwargs#992lukasK9999 wants to merge 1 commit intojcrist:mainfrom
Conversation
Add `msgspec.mypy` plugin that validates keyword arguments passed to
`msgspec.structs.replace()`. Without the plugin, mypy accepts any kwargs
due to the `**changes: Any` signature in the stubs.
The plugin hooks into `get_function_signature_hook` and generates a
call-site-specific signature with the struct's fields as optional keyword
arguments, following the same approach used by mypy's built-in attrs
`evolve()` plugin.
Supports: plain structs, frozen structs, generics, unions of structs,
TypeVar bounds, and Any passthrough.
Usage:
# mypy.ini or pyproject.toml
[mypy]
plugins = [msgspec.mypy]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Mypy support for replace is something I've been missing when using msgspec. We use frozen structs heavily, and without this we usually default to attrs, which has this built in via its mypy plugin. The implementation closely follows mypy's existing attrs evolve() plugin — the core algorithm is essentially the same (get the init signature, extract field names/types, build a typed CallableType), with the only real difference being struct detection via MRO lookup instead of the attrs_attrs marker. It's AI-generated, but I've tested it against all the cases I could think of: plain/frozen structs, generics, unions, |
|
Given that the attrs and dataclass mypy plugin are part of mypy, would it make sense to have this there too ? If you closely copied attrs implementation, there is maybe even room to share common parts ? |
|
attrs is somewhat exceptional historically. It predates dataclasses, and dataclasses were directly inspired by attrs, so mypy’s built-in support for both evolved together around very similar semantics. Since dataclasses are stdlib, core support in mypy is necessary; attrs then remained as an established special case. For newer third-party libraries, the more common pattern is to ship plugin support in the library itself (e.g. Pydantic), which is why keeping msgspec support in msgspec currently seems closer to current practice. |
Summary
msgspec.mypyplugin that validates keyword arguments passed tomsgspec.structs.replace()**changes: Anysignature in the stubs — including invalid field names and wrong typesevolve()plugin: hooks intoget_function_signature_hookand generates a call-site-specific signature with the struct's fields as optional keyword argumentsUsage
What it catches
Supported cases
Struct[T])Test plan
tests/typing/test_mypy_plugin.py— all passingtests/typing/test_mypy.pystill passes (no regression)🤖 Generated with Claude Code